Functions are objects
Inspect JavaScript functions as callable objects: name, length, inferred names, custom properties, constructors, toString, and safe forwarding wrappers.
- 01Inspect a function objectRead
name,length,typeof,instanceof Function, and source text honestly. - 02Use names and properties carefullyExplain inference, named expressions, custom metadata, and minification caveats.
- 03Write safe wrappersForward
thisand arguments withapply, and know whynew Functionis dangerous.
Functions are callable objects
You already know functions as reusable actions: call them with parentheses, pass values in, and get a result back. This lesson adds the other half of the picture: in JavaScript, a function is also an object. It can have properties, it has a prototype chain, and other code can inspect its built-in details.
That is why save.name, save.length, save.count, save.toString(), save.bind(...), and Object.defineProperty(save, ...) all make sense. The function is still callable, but it is also a value with labels attached.
A phone contact can be called, has a name, and can carry a note. A JavaScript function is similar: it can run and also has object properties you can read or add.
- In real life: Tap the contact to call
- In JavaScript: A function can be called
- In real life: The contact has a name
- In JavaScript:
fn.nameis JavaScript’s label - In real life: The contact shows needed details
- In JavaScript:
fn.lengthhints how many leading parameters it expects - In real life: Add a note to the contact
- In JavaScript: Custom properties like
fn.countorfn.meta
Where the analogy stops: A phone contact is easy to rename. Function names can also change, so use them as debugging hints, not core data.
We will cover the topics in the curriculum order: name and length, name inference, custom properties, named function expressions, new Function, generator and async function constructors, Function.prototype.toString, and forwarding wrappers (sometimes called decorators, without the later @ syntax).
name & length: the label and the slots
STEP THROUGHEvery function object has a name string and a length number. name is the best label JavaScript can infer. length is the number of parameters before the first default parameter. Rest parameters never count.
Step through four functions and watch JavaScript record their labels (name) and arity hints (length).
script
return a + b;}const inferred = function (first, second = 2, ...rest) { return first + second + rest.length;};const arrow = (value, index) => value + index;const bound = declared.bind(null, 1);console.log(declared.name, declared.length);console.log(inferred.name, inferred.length);console.log(arrow.name, arrow.length);console.log(bound.name, bound.length);| Kind | Example | name | length |
|---|---|---|---|
| Declaration | function add(a, b) {} | add | 2 |
Anonymous expression assigned to const | const add = function (a, b) {} | add | 2 |
| Default before later parameters | function f(a, b = 1, c) {} | f | 1 |
| Rest parameter | function f(a, ...rest) {} | f | 1 |
| Bound with one argument | add.bind(null, 1) | bound add | 1 |
| Getter descriptor | get total() {} | get total | 0 |
function f(a, b = 1, c) has length === 1, not 3. JavaScript stops counting at b = 1. function g(a, ...rest) also has length === 1; rest is ignored.
Function name inference
INTERACTIVEAnonymous functions often receive useful names from their surroundings. JavaScript writes the variable’s name on an unlabeled tool when you store it: const greet = () => gives greet.name === "greet". Object literal keys, default values, getters, setters, and symbol keys can also provide names.
function add(a, b) { return a + b; }add2functiontruePick a function shape. The details are real JavaScript results verified by the lesson tests; production bundlers may still rewrite names and source text.
The code below shows more inference rules. Assignment gives assigned, method shorthand gives run, a getter descriptor has get total, a symbol method with description becomes [secret], and a default parameter initializer can name the anonymous function fn.
const assigned = function () {};const object = { run() {}, get total() { return 42; }, [Symbol("secret")]() {},};const descriptor = Object.getOwnPropertyDescriptor(object, "total");const fallback = (fn = function () {}) => fn.name;console.log(assigned.name);console.log(object.run.name);console.log(descriptor.get.name);console.log(Object.getOwnPropertySymbols(object).map((key) => object[key].name)[0]);console.log(fallback());Minifiers and bundlers can shorten or rearrange function names. Use fn.name for logs, diagnostics, and friendly labels, not for security decisions or important program logic.
bound save- Rest parameters are ignored
fn.count = 3get totalfunction f(a, b = 1, c) {}is1fn.toString()
Sort each card by whether it describes fn.name, fn.length, or neither.
Custom properties
STEP THROUGHBecause functions are objects, you can attach your own data: fn.count = 0, fn.meta = , or fn.cache = new Map(). That is handy for tiny utilities and debugging. It is also public: any code with the function reference can read or replace the property. Closures, from the Closure patterns lesson, are better for private state.
A function can carry tags. This wrapper counts calls by storing a property on the function object.
script
function makeCounter(fn) { return function counted(...args) { fn.count += 1; return fn(...args); };}function double(n) { return n * 2; }const countedDouble = makeCounter(double);console.log(countedDouble(3));console.log(countedDouble(4));console.log(double.count);function memoize(fn) { function memoized(n) { if (!memoized.cache.has(n)) { memoized.cache.set(n, fn(n)); } return memoized.cache.get(n); } memoized.cache = new Map(); return memoized;}This memoizer stores its cache on the returned function. That makes it easy to inspect or clear with memoized.cache.clear(). If you need the cache to be private, close over a Map instead.
Named function expressions
A named function expression gives an internal name to a function value: const factorial = function inner(n) { … }. The outside variable is factorial. The inside-only name inner is visible inside the function body, which makes recursion safe even if the outer variable is reassigned later.
const factorial = function inner(n) { if (n <= 1) return 1; return n * inner(n - 1);};console.log(factorial(4));try { console.log(inner);} catch (error) { console.log(error.name);}Running that code prints 24, then ReferenceError. inner exists for the recursive call on line 3, but it is not a variable in the outer scope.
The new Function syntax
JavaScript has a constructor that builds a function from strings: new Function("a", "b", "return a + b"). This site never executes it in lesson code. It is included here because you will see it in documentation and older metaprogramming examples.
function demo() { const secret = 7; globalThis.shared = 3; const addShared = new Function("value", "return value + shared;"); console.log(addShared(4)); try { const readSecret = new Function("return secret;"); console.log(readSecret()); } catch (error) { console.log(error.name); }}demo();new Function compiles text as code. That is a security risk with untrusted input, is often blocked by Content Security Policy, and can hurt optimization. It also does not close over local scope: a body created this way can see globals like shared, but not local variables like secret. The tests for this lesson prove that behavior in a sandbox only.
GeneratorFunction, AsyncFunction & AsyncGeneratorFunction
Ordinary Function is not the only function constructor in the language. Generator functions, async functions, and async generator functions each have their own constructor. You almost never call these constructors directly. The useful fact is how to recognize them safely: get the constructor from a real function’s prototype.
const GeneratorFunction = Object.getPrototypeOf(function* () {}).constructor;const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;const AsyncGeneratorFunction = Object.getPrototypeOf(async function* () {}).constructor;console.log(GeneratorFunction.name);console.log(AsyncFunction.name);console.log(AsyncGeneratorFunction.name);This prints GeneratorFunction, AsyncFunction, and AsyncGeneratorFunction. Generators and async code get full lessons later; here we only inspect their object identities.
Function.prototype.toString
Function.prototype.toString returns source text for functions the engine has source for. Built-ins usually return a native-function marker. In a Next.js page, however, the browser receives compiled bundled JavaScript, not your original TypeScript or JSX. So the article uses displayed source strings and tests to verify examples, instead of asking the production bundle to be pretty.
function sample(a) { return a * 2;}console.log(sample.toString().includes("return a * 2"));console.log(Math.max.toString());toString() is useful for curiosity and diagnostics, but it is not a parser API and not a source-map reader. Bundlers, minifiers, and native functions can all make the text different from the file you wrote.
Forwarding wrappers & decorators
STEP THROUGHHere, a decorator means an ordinary wrapper function, not the later @ syntax. A wrapper can add logging, timing, caching, or “run only once” behavior around another function. The key is forwarding: methods need the same this, and callers expect every argument to arrive unchanged.
A wrapper is like putting a present in a gift box with a card. The card adds information, but the receiver should still get the same present.
- In real life: The present inside
- In JavaScript: The original function
- In real life: The box and card
- In JavaScript: The wrapper adds logging, timing, or caching
- In real life: Handing over the same present
- In JavaScript:
fn.apply(this, args)forwards receiver and arguments
Where the analogy stops: A gift box does not change the present. A wrapper can change return values or errors if you write it that way, so keep forwarding behavior explicit.
A decorator is just a wrapper function. The safe version forwards this and all arguments.
script
function withLogging(fn) { function wrapper(...args) { console.log("calling", fn.name, args.join(",")); return fn.apply(this, args); } return wrapper;}const cart = { prefix: "Item", label(id) { return this.prefix + " #" + id; },};cart.label = withLogging(cart.label);console.log(cart.label(7));console.log(cart.label.name);function withLogging(fn) { function wrapper(...args) { console.log("calling", fn.name, args.join(",")); return fn.apply(this, args); } Object.defineProperty(wrapper, "name", { value: "logged" + fn.name[0].toUpperCase() + fn.name.slice(1) }); return wrapper;}const cart = { prefix: "Item", label(id) { return this.prefix + " #" + id; },};cart.label = withLogging(cart.label);console.log(cart.label(7));console.log(cart.label.name);Where you’ll use this
Function-object facts show up in practical code more often than they first appear:
- Logging libraries read
fn.nameto make traces easier to understand, while staying robust when names are missing. - Test helpers and form validators sometimes read
fn.lengthas an arity hint, not as a guarantee. - Memoization utilities store cache metadata on a wrapper or in a closure.
- UI frameworks, analytics, and permission checks wrap callbacks while preserving
thisand arguments.
| Choice | Good for | Trade-off |
|---|---|---|
| Custom property | Inspectable tags like fn.count, fn.cache, or fn.meta | Public and easy to overwrite |
| Closure | Private counters, once flags, hidden memo caches | Harder to inspect or reset from outside |
| WeakMap | Metadata keyed by function without changing it | More code, but avoids property-name collisions |
Common misconceptions
“Functions are not objects because typeof fn is function.”
typeof has a special answer for callable objects. A normal function still has properties and passes fn instanceof Function.
“length means how many arguments were passed.”
fn.length is fixed on the function object. It does not change per call; it counts leading parameters before defaults.
“fn.name is safe for program logic.”
Names are helpful labels, but assignment, bind, and minifiers can change them. Prefer explicit IDs for important behavior.
“Named function expressions leak their name outside.”
The inner name is available inside the function body only. Outside, you use the variable that stores the function.
“A wrapper can just call fn(...args).”
That loses this for methods. Use fn.apply(this, args) when the wrapper should be transparent.
“new Function is a harmless shortcut.”
It compiles a string as code, like eval. Avoid it in app code, especially with user input.
Practice: function objects
5 EXERCISESPredict the second log line exactly.
function save(a, b = 1, ...rest) {}
const wrapped = save.bind(null, "draft");
console.log(save.name, save.length);
console.log(wrapped.name, wrapped.length);The first log is save 1. The bound wrapper reports bound save, and after binding a, no counted leading parameters remain, so the second log is bound save 0.
Read the code and confirm what the final count should be.
function track(fn) {
fn.count = 0;
return function (...args) {
fn.count += 1;
return fn(...args);
};
}
function shout(word) { return word.toUpperCase(); }
const tracked = track(shout);
tracked("hi");
tracked("bye");
console.log(shout.count);function track(fn) {
fn.count = 0;
return function (...args) {
fn.count += 1;
return fn(...args);
};
}
function shout(word) { return word.toUpperCase(); }
const tracked = track(shout);
tracked("hi");
tracked("bye");
console.log(shout.count);shout.count starts at 0. The returned wrapper increments it for each call and then calls the original function. After two calls, the logged count is 2.
thisFix or write the wrapper so the method still sees its object.
function logCalls(fn) {
return function (...args) {
return fn.apply(this, args);
};
}
const user = {
prefix: "User",
label(id) { return this.prefix + ":" + id; },
};
user.label = logCalls(user.label);
console.log(user.label(5));function logCalls(fn) {
return function (...args) {
return fn.apply(this, args);
};
}
const user = {
prefix: "User",
label(id) { return this.prefix + ":" + id; },
};
user.label = logCalls(user.label);
console.log(user.label(5));The wrapper returns fn.apply(this, args), so when user.label(5) calls the wrapper, this is still user. The original method sees this.prefix and prints User:5.
new FunctionExplain why the snippet logs a number and then an error name.
function demo() {
const secret = 7;
globalThis.shared = 3;
const readShared = new Function("return shared;");
console.log(readShared());
try {
const readSecret = new Function("return secret;");
console.log(readSecret());
} catch (error) {
console.log(error.name);
}
}
demo();The first function reads the global shared value and prints 3. The second body tries to read local secret, which is not in its scope, so the catch block prints ReferenceError.
thisMake the wrapper transparent so box.getValue() still works.
function wrap(fn) {
return function (...args) {
return fn.apply(this, args);
};
}
const box = {
value: 9,
getValue() { return this.value; },
};
box.getValue = wrap(box.getValue);
console.log(box.getValue());function wrap(fn) {
return function (...args) {
return fn.apply(this, args);
};
}
const box = {
value: 9,
getValue() { return this.value; },
};
box.getValue = wrap(box.getValue);
console.log(box.getValue());The wrapper is transparent because it forwards both the current this and every argument. box.getValue() therefore returns 9.
Quiz: check your understanding
7 QUESTIONSEvery answer explains itself. Try to predict code output before clicking.
Question 1 of 7What are the inferred name and length?
Read the code, then predictconst greet = function (name, punctuation = "!") {}; console.log(greet.name, greet.length);Choose an answer to see the explanation.
Question 2 of 7Which statement about custom properties on functions is true?
Choose an answer to see the explanation.
Question 3 of 7What is the bound method name?
Read the code, then predictconst obj = { run() {} }; const again = obj.run.bind(obj); console.log(again.name);Choose an answer to see the explanation.
Question 4 of 7Why is
new Functionrisky in application code?Choose an answer to see the explanation.
Question 5 of 7What is the safe core of a forwarding wrapper for methods?
Choose an answer to see the explanation.
Question 6 of 7What is the symbol-keyed method name?
Read the code, then predictconst key = Symbol("job"); const tasks = { [key]() {} }; console.log(tasks[key].name);Choose an answer to see the explanation.
Question 7 of 7Which result should you expect from
Function.prototype.toString?Choose an answer to see the explanation.
Key takeaways
- Functions are callable objects: they can be invoked and inspected.
nameis a useful label;lengthcounts leading parameters before defaults.- Name inference covers variables, object keys, getters, bound functions, defaults, and symbol-keyed methods.
- Custom properties are public object properties; closures are better for private state.
- A named function expression’s inner name is visible inside the function only.
new Functioncompiles strings as code, cannot see local variables, and should be avoided in app code.- Transparent wrappers forward with
fn.apply(this, args).
One-liner.
A JavaScript function is an object with a call button: you can invoke it, read its properties, attach data, and wrap it.
Up next: Methods on primitives.