Scope, lexical environment, and closures (the deep version)
Closures are the question every senior interviewer reaches for when they want to separate "I learned JS from a YouTube playlist" from "I actually understand JS". The good news is the concept is not complicated. The bad news is most explanations make it sound mystical.
Forget the mysticism. A closure is a function plus the memory record it was born next to. That is the whole thing. Let's unpack the parts.
Lexical scope, written not called
Lexical scope means JS decides what variables a function can see based on where you wrote the function in the source code, not where you called it from.
const city = "Bengaluru";
function showCity() {
console.log(city);
}
function caller() {
const city = "Mumbai";
showCity();
}
caller(); // Bengaluru, not MumbaishowCity was written next to the global city, so it sees "Bengaluru". It does not matter that caller had its own city. The link was decided at write time.
That link is called the scope chain. Inner scope first, then outer, then outer's outer, then global. JS walks this chain looking for the name.
Every execution context carries a reference called the lexical environment. It is just a record of, "here are my variables, and here is a pointer to my parent's record." That parent pointer is what makes the chain.
Closures, the chai-stall cash-box
Imagine a chai-stall owner who gives his cousin a small cash-box and sends him out to deliver chai to offices. The cousin walks away from the stall, but the cash-box goes with him. He can still take payments because he is carrying that one box.
A closure is the same. A function that gets returned from another function carries its outer lexical environment with it, even after the outer function has finished.
function makeWallet(initial) {
let balance = initial;
return function spend(amount) {
balance -= amount;
return balance;
};
}
const wallet = makeWallet(1000);
wallet(150); // 850
wallet(200); // 650Step-by-step.
- Step 1.
makeWallet(1000)is called. New EC pushed.balance = 1000. - Step 2. It returns the inner
spendfunction. The outer EC pops off the stack. - Step 3. Normally
balancewould be garbage-collected. Butspendstill references it, so JS keeps that lexical environment alive. - Step 4.
wallet(150)callsspend. It walks the scope chain, findsbalancein the preserved environment, mutates it. - Step 5. Next call sees the updated
850.
Interview trap: a closure does not snapshot values. It holds a live reference. Mutate
balanceand every call tospendsees the new value.
The famous for var setTimeout trap
This one shows up in every interview, every year.
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("Order", i), 100);
}
// Output: Order 4, Order 4, Order 4Why? var is function-scoped, so there is only one i shared by all three callbacks. By the time the timers fire, the loop has finished and i is 4. All three closures point at the same i.
Fix with let.
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log("Order", i), 100);
}
// Output: Order 1, Order 2, Order 3let is block-scoped. Each iteration of the loop creates a fresh i in a new lexical environment. Each callback closes over its own copy. This is not a bug fix, it is JS doing exactly what the spec says.
Closures and memory, the leak nobody talks about
A closure only keeps the variables it actually references. JS engines are smart about this. But you can still leak.
function loadOrders() {
const huge = new Array(1_000_000).fill("order");
return function getCount() {
return huge.length;
};
}
const counter = loadOrders(); // huge stays in memory foreverhuge is referenced by getCount, so the million-element array cannot be collected. If you only need the length, pull it out before returning.
function loadOrders() {
const huge = new Array(1_000_000).fill("order");
const length = huge.length;
return function getCount() { return length; };
}Now huge is unreferenced after loadOrders returns and the garbage collector can free it.
Quick reference
- Lexical Scope
- Variable visibility decided by where a function is written in the source code.
- Scope Chain
- The ordered list of lexical environments JS walks to resolve a variable name.
- Lexical Environment
- The memory record holding a scope's variables plus a pointer to its parent scope.
- Closure
- A function bundled with the lexical environment it was defined in, so it can access outer variables even after the outer function has returned.
Predict the output
function counter() {
let n = 0;
return () => ++n;
}
const a = counter();
const b = counter();
console.log(a(), a(), b()); // ?Each counter() call creates a fresh n in a fresh lexical environment. a and b have separate boxes. Output is 1 2 1.
Senior rule: a closure is a function plus the lexical environment it was born in. Not a snapshot, a live reference.
Quick quiz, prove you got it
- 1.A closure remembers...
- 2.What does this print? `for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);`
- 3.Does a closure prevent the entire outer scope from being garbage collected?
Interview insights
Closure interview answer
- Definition: function + lexical environment in which it was defined
- A real example (counter or once or private state)
- The for-var loop trap and how let fixes it
- Memory consideration: closures can hold large data alive
takeaway: Closure is not "function inside a function". It is the linked environment that persists.
Tricky questions they will ask
Q.Why does this print 1 then 1, not 1 then 2? `function f() { let n = 1; return () => console.log(n); } const a = f(); const b = f(); a(); b();`›
A.Each call to f() creates a new lexical environment. a and b are TWO separate closures, each over its OWN n. They look the same but are independent.
Q.When does a closure actually leak memory?›
A.When the closure captures a large object that the inner function does not even use. Modern engines try to optimise this away, but if you assign the closure to a long-lived variable (a DOM listener, a global, a timer), the captured data stays alive as long as the closure does.
Free tools you can use while you learn
Common questions
Q.In one sentence, what is a closure?›
Q.Does a closure hold the entire outer scope in memory?›
Watching quietly. Tap me if you want a tip.
Try this (0 of 2 done)
- 1
Make the loop log 0, 1, 2 (not 3, 3, 3) by changing var to let.
show answer
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } - 2
Build a private counter using a closure. Call it 3 times and log results.
show answer
function makeCounter() { let n = 0; return () => ++n; } const tick = makeCounter(); console.log(tick(), tick(), tick());