Function Objects: What a Function IS in Memory
Function sirf code nahi, ek aisa object hai jismein jaan hai — [[Call]] aur [[Scope]] ki.
In JavaScript, a function is not just a "block of code" — it is a full-fledged object that also happens to be callable. Every function you create is an instance of Function, which itself inherits from Object.prototype.
This means functions can have properties attached to them, just like any other object. The special internal slot [[Call]] is what makes a function invocable — you can use () on it. Regular functions also have [[Construct]], which allows the new keyword. Arrow functions lack [[Construct]].
function greet(name) {
return 'Hello, ' + name;
}
// Functions are objects — add properties!
greet.language = 'Hinglish';
greet.defaultGreeting = 'Namaste';
console.log(greet.language); // 'Hinglish'
console.log(greet.defaultGreeting); // 'Namaste'
console.log(typeof greet); // 'function'
console.log(greet instanceof Object); // true
// First-class citizen: stored in array
const operations = [
function add(a, b) { return a + b; },
function multiply(a, b) { return a * b; }
];
console.log(operations[0](5, 3)); // 8
console.log(operations[1](5, 3)); // 15
name, length), and crucially, pointers to its compiled Code object and its [[Scope]] (the Lexical Environment where it was defined).Every function object carries several internal slots that V8 uses to determine its behavior. These are not directly accessible from JavaScript but they power everything:
[[Scope]]: The Lexical Environment captured when the function is defined. This is the mechanism behind closures — the function "remembers" variables from its birthplace.
[[Call]]: The actual compiled code to execute. Present in ALL functions — without this, it would not be a function.
[[Construct]]: Only present in regular functions. Allows new keyword. Arrow functions lack this — calling new ArrowFn() throws TypeError.
[[HomeObject]]: Used by super in class methods. Bound where the method is defined, not where it is called.
// [[Scope]] in action — closure creation
function outer() {
const secret = 'Hyderabadi Biryani';
// This function captures outer's Lexical Environment in [[Scope]]
function inner() {
console.log(secret); // accessed via [[Scope]]
}
return inner;
}
const fn = outer();
fn(); // 'Hyderabadi Biryani' — secret lives inside fn.[[Scope]]
// [[Construct]] — arrows can't be constructed
const Arrow = () => {};
const Regular = function() {};
// new Arrow(); // TypeError: Arrow is not a constructor
new Regular(); // OK — Regular has [[Construct]]
// .name and .length
function add(a, b) { return a + b; }
console.log(add.name); // 'add'
console.log(add.length); // 2 (counts declared parameters)
const myFn = function(x, y, z) { return x; };
console.log(myFn.name); // 'myFn' (inferred from variable!)
console.log(myFn.length); // 3
Function objects come with several built-in properties that V8 provides automatically:
.name: The function's name. In modern JS, this is inferred even for anonymous expressions from the variable or property it was assigned to. Incredibly useful for debugging and stack traces.
.length: Counts declared parameters before the first default value. Does NOT count rest parameters or parameters after a default.
.prototype: Only exists on non-arrow functions. Used as the prototype for objects created with new. Arrow functions do NOT have this property.
// .name inference magic
const myFunc = function() {};
console.log(myFunc.name); // 'myFunc'
const obj = {
method: function() {}
};
console.log(obj.method.name); // 'method'
// .length quirks
function api(endpoint, method='GET', options) {}
console.log(api.name); // 'api'
console.log(api.length); // 1 — only 'endpoint' counts before default!
function withRest(a, b, ...rest) {}
console.log(withRest.length); // 2 — rest parameters don't count
// .prototype — only on constructable functions
function Person(name) { this.name = name; }
console.log(Person.prototype); // { constructor: f } — exists!
Person.prototype.greet = function() { return this.name; };
const Arrow = () => {};
console.log(Arrow.prototype); // undefined — arrows don't have it
.name inference is incredibly useful for debugging. Even if you pass an anonymous function to addEventListener, V8 can often infer its name from the variable or property it was assigned to, making stack traces readable.How functions are created in memory depends on whether you use a Declaration or Expression:
Declaration: function f() {} — Hoisted completely (name AND body). Created in the Variable Environment during the creation phase. You can call it before its definition.
Expression: const f = function() {} — Only the variable is hoisted (set to undefined). The function object is created during the execution phase when that line is reached.
Named Function Expression: const f = function myFunc() {} — The name myFunc is only available inside the function body, not in the outer scope.
// Hoisting difference
console.log(declaredFn()); // 'I work!' — fully hoisted
// console.log(expressFn()); // TypeError: expressFn is not a function
function declaredFn() { return 'I work!'; }
const expressFn = function() { return 'Later!'; };
// Named Function Expression
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // 'fact' available inside!
};
console.log(factorial(5)); // 120
// console.log(fact); // ReferenceError — 'fact' not in outer scope!
// V8 Lazy Compilation example
function app() {
// This function is parsed but NOT compiled to bytecode yet
function heavySetup() {
// complex logic...
return 'Setup complete';
}
if (false) {
heavySetup(); // V8 might never compile this if never called!
}
}
Under the hood, V8 creates several objects for each function:
Each function gets a Code object on the V8 heap containing its bytecode (generated by the Ignition interpreter). When a function becomes "hot" (called many times), TurboFan compiles it to optimized machine code.
Function objects also maintain a FeedbackVector storing inline caches (IC) for property access and argument types. This is why changing the "shape" of objects passed to a function causes deoptimization — V8 has to throw away its optimized assumptions.
// V8 FeedbackVector in action
function processUser(user) {
return user.name; // V8 records: "first call saw {name: string}"
}
const user1 = { name: 'Sai', age: 25 };
const user2 = { name: 'Ali', age: 30 };
processUser(user1); // V8 records: monomorphic IC for user.name
processUser(user2); // Same shape (name, age) — IC hit! Fast path.
const admin = { name: 'Ravi', role: 'Admin' }; // Different shape!
processUser(admin); // V8 must update IC to polymorphic. Slightly slower.
// Function as state container (unusual but valid)
function counter() {
counter.count++;
return counter.count;
}
counter.count = 0; // property on the function object itself
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Lo kar liya — Key Points:
- ✅ Functions are objects with internal slots: [[Call]] makes them runnable, [[Scope]] stores their closure, [[Construct]] allows
newkeyword - ✅ Arrow functions lack [[Construct]] and
.prototype— they cannot be used as constructors - ✅
.nameis inferred in modern JS even for anonymous expressions;.lengthcounts parameters before defaults/rest - ✅ Function declarations are fully hoisted; expressions are not — the function object is created at runtime
- ✅ V8 uses lazy compilation — functions are parsed but not compiled to bytecode until their first call
- ✅ Every function maintains a FeedbackVector storing IC data, which TurboFan uses for optimization
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login