Understanding Function Objects and Named Function Expressions in JavaScript
In JavaScript, functions are not just executable blocks of code. They are full-fledged objects. This means they can have properties, methods, and be passed around like any other value. Every function you write automatically comes with built-in properties like name and length, and you can add your own custom properties to any function.
This guide explores the object nature of functions in depth, explains the built-in properties every function carries, shows how custom properties turn functions into stateful entities, and introduces Named Function Expressions (NFE), a subtle but important feature that enables reliable self-reference in function expressions. Understanding these concepts deepens your grasp of how JavaScript treats functions as first-class citizens.
Functions Are Objects: Properties and Methods
In JavaScript, functions are a special type of object. They are instances of the built-in Function constructor and inherit from Function.prototype. Like any object, they can hold properties and be inspected at runtime.
Proving Functions Are Objects
function greet(name) {
return `Hello, ${name}!`;
}
// Functions have a type
console.log(typeof greet); // "function"
// But they are also objects
console.log(greet instanceof Object); // true
// You can check their constructor
console.log(greet.constructor === Function); // true
// You can enumerate their properties
console.log(Object.getOwnPropertyNames(greet));
// ['length', 'name', 'arguments', 'caller', 'prototype']
The typeof operator returns "function" (not "object") as a special case, but functions are fundamentally objects with an internal [[Call]] method that makes them callable.
Functions Have Methods
Since functions inherit from Function.prototype, they have access to several built-in methods:
function multiply(a, b) {
return a * b;
}
// .call() (invoke with a specific "this" and arguments)
console.log(multiply.call(null, 3, 4)); // 12
// .apply() (invoke with a specific "this" and an array of arguments)
console.log(multiply.apply(null, [3, 4])); // 12
// .bind() (create a new function with a bound "this" and/or partial arguments9
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
console.log(double(10)); // 20
// .toString() (get the source code of the function)
console.log(multiply.toString());
// "function multiply(a, b) {\n return a * b;\n}"
Functions Can Be Stored, Passed, and Returned
Because functions are objects, they can be treated like any other value:
// Store in a variable
const fn = function(x) { return x * 2; };
// Store in an array
const operations = [
(x) => x + 1,
(x) => x * 2,
(x) => x ** 2
];
// Store as an object property (a method)
const math = {
double: (x) => x * 2,
square: (x) => x ** 2
};
// Pass as an argument
function applyOperation(value, operation) {
return operation(value);
}
console.log(applyOperation(5, math.double)); // 10
console.log(applyOperation(5, math.square)); // 25
// Return from another function
function createAdder(n) {
return function(x) {
return x + n;
};
}
const add10 = createAdder(10);
console.log(add10(5)); // 15
The name Property
Every function in JavaScript has a name property that contains the function's name as a string. This property is set automatically by the engine and is incredibly useful for debugging.
Function Declarations
function sayHello() {
console.log("Hello!");
}
console.log(sayHello.name); // "sayHello"
Function Expressions
Even when a function is anonymous (has no explicit name), JavaScript infers the name from the variable it is assigned to:
const sayGoodbye = function() {
console.log("Goodbye!");
};
console.log(sayGoodbye.name); // "sayGoodbye" (inferred from the variable name)
This is called contextual naming. The engine looks at the context of the assignment and uses it to set the name property.
Arrow Functions
Arrow functions also get their names inferred from the assignment:
const multiply = (a, b) => a * b;
console.log(multiply.name); // "multiply"
Object Methods
Method names are inferred from the property name:
const user = {
greet() {
console.log("Hi!");
},
farewell: function() {
console.log("Bye!");
},
wave: () => {
console.log("👋");
}
};
console.log(user.greet.name); // "greet"
console.log(user.farewell.name); // "farewell"
console.log(user.wave.name); // "wave"
Default Parameter Functions
function process(callback = function() {}) {
console.log(callback.name);
}
process(); // "callback" (inferred from the parameter name)
When Name Inference Fails
There are situations where the engine cannot infer a name:
// Array elements (no name to infer from)
const handlers = [
function() { return 1; },
function() { return 2; }
];
console.log(handlers[0].name); // "" (empty string)
console.log(handlers[1].name); // "" (empty string)
// Immediately returned from another function
function createHandler() {
return function() { return "handled"; };
}
console.log(createHandler().name); // "" (empty string)
Why the name Property Matters
The name property appears in stack traces, error messages, and debugging tools. A meaningful name makes debugging dramatically easier:
// With a named function
function processPayment() {
throw new Error("Payment failed");
}
// Stack trace will show:
// Error: Payment failed
// at processPayment (script.js:2:9)
// With an anonymous function
const processPayment2 = function() {
throw new Error("Payment failed");
};
// Stack trace will show:
// Error: Payment failed
// at processPayment2 (script.js:2:9)
// (Name was inferred from the variable, so it's still readable)
The name Property Is Read-Only (Non-Writable, but Configurable)
function example() {}
console.log(example.name); // "example"
example.name = "something_else";
console.log(example.name); // "example" (unchanged! name is non-writable)
// But it IS configurable, so you can redefine it with Object.defineProperty
Object.defineProperty(example, "name", { value: "customName" });
console.log(example.name); // "customName"
The length Property (Number of Parameters)
Every function has a length property that returns the number of expected parameters defined in the function signature. This does not include rest parameters or parameters with default values.
Basic Usage
function noParams() {}
function oneParam(a) {}
function twoParams(a, b) {}
function threeParams(a, b, c) {}
console.log(noParams.length); // 0
console.log(oneParam.length); // 1
console.log(twoParams.length); // 2
console.log(threeParams.length); // 3
Rest Parameters Are Not Counted
function withRest(a, b, ...rest) {}
console.log(withRest.length); // 2 (rest parameter is excluded)
Default Parameters Affect the Count
Parameters with default values and everything after them are not counted:
function withDefaults(a, b = 10, c) {}
console.log(withDefaults.length); // 1 (only "a" is counted)
function allDefaults(a = 1, b = 2, c = 3) {}
console.log(allDefaults.length); // 0
function defaultInMiddle(a, b = 5, c, d) {}
console.log(defaultInMiddle.length); // 1 (stops counting at the first default)
The rule is: length counts the number of parameters before the first one with a default value.
Practical Use: Polymorphic Behavior Based on length
The length property is useful in functions that accept other functions as arguments and need to behave differently based on how many parameters the callback expects:
function ask(question, ...handlers) {
const isYes = confirm(question);
for (const handler of handlers) {
if (handler.length === 0) {
// Handler takes no arguments (call it if user answered "yes")
if (isYes) handler();
} else {
// Handler takes an argument (pass the answer)
handler(isYes);
}
}
}
ask(
"Do you agree?",
() => console.log("You agreed!"), // length: 0
(result) => console.log(`Your answer: ${result}`) // length: 1
);
A More Practical Example: Middleware-Style Processing
function runMiddleware(middleware, req, res) {
if (middleware.length === 2) {
// Standard middleware: (req, res)
middleware(req, res);
} else if (middleware.length === 3) {
// Error-handling middleware: (err, req, res)
middleware(null, req, res);
}
}
This pattern is actually used by Express.js to distinguish between regular middleware and error-handling middleware. Express checks function.length to determine if a middleware function expects an error parameter.
Summary of length Rules
function test1(a, b, c) {} // length: 3
function test2(a, b, c = 1) {} // length: 2
function test3(a = 1, b, c) {} // length: 0
function test4(...args) {} // length: 0
function test5(a, b, ...args) {} // length: 2
function test6(a, b = 2, ...args) {} // length: 1
| Parameter type | Counted in length? |
|---|---|
| Regular parameters | Yes |
| Parameters with defaults | No (and stops counting) |
Rest parameter ...args | No |
Custom Properties on Functions
Since functions are objects, you can add your own properties to them. This is a legitimate and sometimes useful pattern in JavaScript.
Adding Properties
function sayHi() {
console.log("Hi!");
sayHi.callCount++;
}
sayHi.callCount = 0;
sayHi(); // Hi!
sayHi(); // Hi!
sayHi(); // Hi!
console.log(sayHi.callCount); // 3
The callCount property lives on the function object, not in a variable. It is accessible wherever sayHi is accessible.
Function Properties Are Not Variables
It is important to understand that a property on a function is not the same as a local variable inside it:
function greet() {
let counter = 0; // Local variable (reset on every call)
counter++;
console.log(`Local counter: ${counter}`);
greet.persistentCounter++;
console.log(`Persistent counter: ${greet.persistentCounter}`);
}
greet.persistentCounter = 0;
greet();
// Local counter: 1
// Persistent counter: 1
greet();
// Local counter: 1 ← reset each call
// Persistent counter: 2 ← persists across calls
greet();
// Local counter: 1
// Persistent counter: 3
Custom Properties vs. Closures
The call-counting functionality above could also be achieved with a closure:
// Using a closure
function makeCounter() {
let count = 0;
function counter() {
count++;
return count;
}
counter.getCount = function() {
return count;
};
return counter;
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
console.log(counter.getCount()); // 2
// Using a function property
function counter() {
counter.count++;
return counter.count;
}
counter.count = 0;
counter(); // 1
counter(); // 2
console.log(counter.count); // 2
The key difference:
- Closure variables are private. External code cannot access
countdirectly; it can only use the exposed methods. - Function properties are public. Anyone with a reference to the function can read and modify
counter.count.
// With function property (external code can tamper with it)
counter.count = 1000; // Allowed!
console.log(counter()); // 1001
// With closure (external code cannot tamper)
// count is not accessible from outside
Choose closures when the data should be private and tamper-proof. Choose function properties when the data should be publicly readable or when you want a simpler, more transparent approach.
Real-World Example: Caching with Function Properties
function factorial(n) {
if (n in factorial.cache) {
return factorial.cache[n];
}
if (n <= 1) {
factorial.cache[n] = 1;
} else {
factorial.cache[n] = n * factorial(n - 1);
}
return factorial.cache[n];
}
factorial.cache = {};
console.log(factorial(5));
// 120
console.log(factorial(10));
// 3628800
console.log(factorial.cache);
// {1: 1, 2: 2, 3: 6, 4: 24, 5: 120, 6: 720, 7: 5040, 8: 40320, 9: 362880, 10: 3628800}
The cache is stored directly on the function object, making it transparent and easy to inspect or clear:
// Easy to inspect
console.log(Object.keys(factorial.cache).length); // 10 entries
// Easy to clear
factorial.cache = {};
Named Function Expressions: What and Why
A Named Function Expression (NFE) is a function expression that has a name in its definition. It looks like a regular function expression but includes a name between the function keyword and the parentheses:
// Regular (anonymous) function expression
const sayHi = function() {
console.log("Hi!");
};
// Named Function Expression (NFE)
const sayHello = function greeting() {
console.log("Hello!");
};
At first glance, sayHello and greeting might seem redundant. But they serve different purposes, and the name greeting has two special characteristics:
- It is only accessible inside the function itself
- It is not overwritten even if the outer variable is reassigned
The NFE Name Is Internal Only
const sayHello = function greeting() {
console.log("Hello!");
console.log(typeof greeting); // "function" (accessible inside)
};
sayHello();
console.log(typeof greeting); // "undefined" (NOT accessible outside!)
The name greeting exists only within the function body. It does not leak into the surrounding scope. The outer scope only knows about sayHello.
The NFE Name Appears in Stack Traces
Even though the name is internal, it shows up in error messages and stack traces, which aids debugging:
const handler = function processRequest() {
throw new Error("Something went wrong");
};
handler();
// Error: Something went wrong
// at processRequest (script.js:2:9)
Without the NFE name, the stack trace would show the inferred name from the variable. In many cases, this works just as well. But when functions are passed as callbacks, stored in arrays, or created dynamically, the NFE name provides a stable, reliable label.
The name Property with NFE
The NFE name takes priority over the inferred name for the name property:
const func = function myName() {};
console.log(func.name); // "myName" (the NFE name, not "func")
NFE for Reliable Self-Reference in Recursion
The main practical reason to use an NFE is reliable self-reference. When a function needs to call itself (recursion) and it is assigned to a variable, there is a risk that the variable might be reassigned, breaking the recursive call.
The Problem: Broken Self-Reference
const factorial = function(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // References the variable "factorial"
};
console.log(factorial(5)); // 120 (works fine)
// But what if we copy and reassign?
const originalFactorial = factorial;
const factorial2 = factorial; // Save a reference
// Imagine someone overwrites the original variable
// (This can happen in complex codebases, testing, monkey-patching, etc.)
Let's see a concrete scenario where this breaks:
let sayHi = function(who) {
if (who) {
console.log(`Hello, ${who}`);
} else {
sayHi("World"); // Recursive call via the variable name
}
};
const greet = sayHi; // Copy the reference
sayHi = null; // Overwrite the original variable
greet("Alice"); // "Hello, Alice" (works, no recursion needed)
greet(); // TypeError: sayHi is not a function!
// The function tried to call sayHi(), but sayHi is now null
The function references itself through the variable sayHi, but that variable was reassigned. The function's self-reference is broken.
The Solution: Named Function Expression
Using an NFE gives the function an internal name that cannot be overwritten from outside:
let sayHi = function greeting(who) {
if (who) {
console.log(`Hello, ${who}`);
} else {
greeting("World"); // Uses the NFE name (always reliable)
}
};
const greet = sayHi;
sayHi = null;
greet("Alice"); // "Hello, Alice" (works)
greet(); // "Hello, World" (also works!)
The name greeting is bound within the function's own scope and cannot be changed from outside. Even if the outer variable sayHi is reassigned, deleted, or garbage collected, the internal name greeting still points to the function itself.
NFE Name Cannot Be Reassigned Internally
The NFE name behaves like a const inside the function. You cannot reassign it:
const func = function myFunc() {
// myFunc = 42; // This silently fails in non-strict mode
// In strict mode, it would throw a TypeError
console.log(typeof myFunc); // "function" (always the function itself)
};
This guarantees that the self-reference is always reliable.
Recursive Factorial with NFE
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // "fact" is always this function
};
console.log(factorial(5)); // 120
// Safe even if the variable is reassigned
const myFactorial = factorial;
// Even if someone later does: factorial = null;
// myFactorial still works because it uses "fact" internally
NFE in Practice: Event Handlers That Remove Themselves
NFEs are useful when a callback needs to reference itself, such as a one-time event handler that removes itself:
document.addEventListener("click", function handleClick(event) {
console.log("Clicked!", event.target);
// Remove this specific handler
document.removeEventListener("click", handleClick);
});
// The handler fires once, then removes itself using its own NFE name
Without the NFE name, you would need to store the function in a separate variable first:
// Without NFE (requires a separate variable)
const handleClick = function(event) {
console.log("Clicked!", event.target);
document.removeEventListener("click", handleClick);
};
document.addEventListener("click", handleClick);
Both approaches work, but the NFE version is more self-contained.
NFE vs. Function Declarations for Self-Reference
Function declarations have their name available internally too, so this problem does not arise:
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // "factorial" is the declared name
}
However, function declarations are hoisted and always named in the outer scope. NFEs exist specifically for situations where you need a function expression (assignment to a variable, passing as an argument, returning from another function) but also want a reliable internal name.
When NFE Does Not Apply
Arrow functions cannot be NFEs. Arrow functions do not have their own name syntax. Their name property is always inferred from the assignment:
// Arrow functions cannot have an explicit name
const add = (a, b) => a + b;
console.log(add.name); // "add" (inferred)
// There is no syntax for: const add = addFn(a, b) => a + b;
If you need self-reference in an arrow function, you must reference the outer variable:
const countdown = (n) => {
if (n <= 0) return;
console.log(n);
countdown(n - 1); // Must use the variable name (no NFE available)
};
Putting It All Together
Here is an example that combines function properties, the name and length properties, and NFE:
const createValidator = function validator(schema) {
// NFE name "validator" for reliable self-reference
function validate(data) {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
if (rules.required && !(field in data)) {
errors.push(`${field} is required`);
}
if (rules.type && typeof data[field] !== rules.type) {
errors.push(`${field} must be of type ${rules.type}`);
}
if (rules.min && data[field] < rules.min) {
errors.push(`${field} must be at least ${rules.min}`);
}
}
validate.lastResult = errors.length === 0;
validate.errorCount++;
return {
valid: errors.length === 0,
errors
};
}
// Custom properties on the function
validate.errorCount = 0;
validate.lastResult = null;
// Use function properties
console.log(`Validator name: ${validator.name}`); // "validator"
console.log(`Schema expects: ${Object.keys(schema).length} fields`);
return validate;
};
const validateUser = createValidator({
name: { required: true, type: "string" },
age: { required: true, type: "number", min: 0 },
email: { required: true, type: "string" }
});
console.log(validateUser({ name: "Alice", age: 30, email: "a@b.com" }));
// { valid: true, errors: [] }
console.log(validateUser({ name: "Bob" }));
// { valid: false, errors: ['age is required', 'email is required'] }
console.log(validateUser.errorCount); // 2
console.log(validateUser.lastResult); // false
// Function introspection
console.log(validateUser.name); // "validate"
console.log(validateUser.length); // 1 (expects one parameter: data)
Summary
| Concept | Key Takeaway |
|---|---|
| Functions are objects | Functions can have properties, methods, and be passed around like any value |
name property | Automatically set; inferred from variable, property, or parameter name |
length property | Number of declared parameters before the first default or rest parameter |
| Custom properties | You can add any property to a function; it persists across calls and is publicly accessible |
| Custom properties vs. closures | Properties are public and transparent; closures are private and encapsulated |
| Named Function Expression | A function expression with an explicit name: function name() {} |
| NFE name scope | The NFE name is only accessible inside the function body, not outside |
| NFE for self-reference | Provides a reliable, unbreakable reference to the function itself for recursion or self-removal |
| Arrow functions | Cannot be NFEs; always infer their name from the assignment context |
Understanding that functions are objects opens up a new dimension of JavaScript programming. You can store state on functions, introspect their parameter counts, leverage their names for better debugging, and use Named Function Expressions for bulletproof self-reference. These are not exotic tricks but fundamental aspects of how JavaScript works, and you will encounter them constantly in real-world code, libraries, and frameworks.