Closures: Memory Layout, Context Chain & Patterns
Stack marta hai, heap zinda rehta โ closure ka asli chehra.
A closure is a function bundled together with the Lexical Environment it was created in. When an inner function is created, V8 attaches a reference to its outer scope's Lexical Environment via an internal [[Environment]] slot.
When the outer function returns, its stack frame is destroyed โ that's how call stacks work. But the Lexical Environment survives on the heap because the inner function holds a reference to it. The garbage collector cannot collect that Lexical Environment while the inner function is alive.
Every function in JavaScript is technically a closure โ even top-level functions close over the global Lexical Environment. V8's implementation: each function object has an internal [[Environment]] slot pointing to the Lexical Environment where it was created.
function makeCounter() {
let count = 0; // lives in makeCounter's Lexical Environment
return function() { // inner function โ closes over the LE above
count++; // reads and writes count from captured LE
return count;
};
}
const counter = makeCounter();
// makeCounter's stack frame: GONE (it returned)
// But makeCounter's Lexical Environment: ALIVE (counter holds reference)
// count: 0 โ 1 โ 2 โ 3 ...
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// Two separate calls = two separate Lexical Environments
const c1 = makeCounter(); // LE1: { count: 0 }
const c2 = makeCounter(); // LE2: { count: 0 }
c1(); c1(); // LE1: { count: 2 }
c2(); // LE2: { count: 1 }
console.log(c1()); // 3 โ LE1 and LE2 are INDEPENDENT
console.log(c2()); // 2
Not ALL variables in a function go to the heap โ only captured ones. V8 analyzes which variables are referenced by inner functions during compilation.
Captured variables: allocated in a Context object on the heap. Non-captured variables: allocated on the stack (much faster, no GC pressure).
If only SOME vars are captured, only those go to the Context. This analysis happens at compile time (bytecode generation) โ not at runtime.
function example() {
let captured = 0; // CAPTURED โ goes to Context object on heap
let notCaptured = 0; // NOT captured โ stays on stack (faster!)
const timer = setInterval(() => {
captured++; // references captured โ heap access
// notCaptured is not used in any inner function โ stack access
}, 1000);
return function() {
return captured; // inner function references captured โ proves it's captured
};
}
// V8 at compile time:
// 1. Scans example() and all inner functions
// 2. Finds 'captured' is referenced in both outer AND inner functions
// 3. Marks 'captured' as context-allocated โ will live on heap
// 4. 'notCaptured' not referenced in inner functions โ stack allocated
// Real-world implication:
function processItems(items) {
const LIMIT = 100; // captured? only if inner fn uses it
let processed = 0; // captured if inner fn uses it
return items.map(item => { // arrow fn is inner
processed++; // captured โ referenced in arrow fn
return item.value * 2; // LIMIT not used โ stack if not referenced
});
}
Closures power some of the most important patterns in JavaScript. Here are the classics you'll use constantly:
1. Counter factory: multiple independent counters from one factory function.
2. Memoization: cache function results using closure over a Map โ the Map persists across calls.
3. Once function: closure tracks if function has been called โ runs only the first time.
// 1. Counter factory
function makeCounter(start = 0, step = 1) {
let count = start;
return {
increment() { count += step; return count; },
decrement() { count -= step; return count; },
reset() { count = start; return count; },
value() { return count; }
};
}
const c = makeCounter(10, 5);
console.log(c.increment()); // 15
console.log(c.increment()); // 20
console.log(c.reset()); // 10
// 2. Memoization with closure
function memoize(fn) {
const cache = new Map(); // captured in closure โ persists!
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize(n => {
console.log('Computing...');
return n * n * n;
});
console.log(expensiveCalc(5)); // Computing... 125
console.log(expensiveCalc(5)); // 125 (from cache โ no log!)
// 3. Once function โ runs only the first time
function once(fn) {
let called = false; // captured
let result; // captured
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return 'initialized';
});
console.log(initialize()); // 'Initializing...' 'initialized'
console.log(initialize()); // 'initialized' โ no log, cachedA closure keeps the ENTIRE Lexical Environment alive โ not just the variables you use. This can cause memory leaks when large objects are captured unnecessarily.
The problem: Large objects captured by closure cannot be GC'd even if only a tiny property is used. The fix: Extract only what you need into a local variable before creating the closure.
Long-lived closures (event listeners, timers, observers) are the most common leak sources. Always store cleanup references and call removeEventListener / clearInterval / disconnect.
// Closure memory leak โ large data captured unnecessarily
function setupHandler(bigData) {
// bigData is 50MB of fetched data
const summary = bigData.summary; // just a string!
document.addEventListener('click', function() {
console.log(summary);
// closure captures the ENTIRE LE of setupHandler
// bigData stays in memory as long as this listener exists!
});
}
// Fix: extract only what the closure needs
function setupHandlerFixed(bigData) {
const summary = bigData.summary; // extract first
bigData = null; // release reference before closure creation
document.addEventListener('click', function() {
console.log(summary); // closure only captures 'summary' string โ tiny!
});
}
// Timer leak โ closure prevents GC
function startPolling() {
const data = fetchData(); // captured by closure
const timerId = setInterval(() => {
console.log(data.status); // data stays alive forever
}, 1000);
// timerId never cleared โ interval and closure live forever!
return timerId; // return so caller can clearInterval
}
const id = startPolling();
// Cleanup when done:
clearInterval(id);
Closures are everywhere in real-world JavaScript. Here are the patterns you'll use daily:
1. Debounce: timer ID captured in closure across calls โ cancel previous, set new timer.
2. Currying: function that returns function that returns function โ closure chain accumulates arguments.
3. Private data: closure over variables not on this โ true encapsulation before # private fields.
// 1. Debounce โ timer captured in closure
function debounce(fn, delay) {
let timer = null; // captured across ALL calls
return function(...args) {
clearTimeout(timer); // cancel previous timer
timer = setTimeout(() => { // new timer stored in closure
fn.apply(this, args);
}, delay);
};
}
const search = debounce(query => {
console.log('Searching:', query);
}, 300);
search('H'); // timer set
search('Hy'); // prev timer cancelled, new timer
search('Hyd'); // prev timer cancelled, new timer
// Only 'Hyd' fires after 300ms
// 2. Currying with closure chain
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
function add(a, b, c) { return a + b + c; }
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
// 3. Private data with closure (before # private fields)
function createBankAccount(initialBalance) {
let balance = initialBalance; // truly private โ not on 'this'
return {
deposit(amount) { balance += amount; return balance; },
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
getBalance() { return balance; }
// balance is NOT on the returned object โ cannot be accessed directly!
};
}
const account = createBankAccount(1000);
// account.balance โ undefined (not on the object!)
console.log(account.getBalance()); // 1000 โ
console.log(account.deposit(500)); // 1500
Lo kar liya โ Key Points:
- โ Closure = function + its captured Lexical Environment โ the LE lives on the heap even after the outer function returns
- โ V8 context-allocates only CAPTURED variables onto the heap โ non-captured variables stay on the (faster) stack
- โ Each call to the outer function creates a SEPARATE Lexical Environment โ multiple closures from one factory are independent
- โ Closures keep their entire LE alive โ extract only needed data before creating closures over large objects
- โ Debounce, memoize, once, counter factory, currying โ all are closure patterns you use constantly
- โ Clean up long-lived closures (event listeners, timers) to prevent memory leaks
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