Day 2: Scope Chain, Lexical Environment & Closures
See closures as a natural consequence of how the engine links execution contexts together via the scope chain — not a special trick.
Study
Concepts
Lexical Environment = variables + a pointer to the parent
Every execution context has a Lexical Environment: a record of its own variables plus a reference to its parent (outer) Lexical Environment. "Lexical" means this parent link is decided by *where the function is written in the source*, not by who calls it.
When you reference an identifier, the engine looks it up in the current environment record; if not found, it walks the parent pointer outward — repeating until it hits the Global environment or throws a ReferenceError. This chain of parent pointers is the Scope Chain.
Closures: functions that remember their birth scope
A closure is what you get when an inner function is created inside an outer function and keeps a live reference to the outer Lexical Environment, even after the outer function has returned and been popped off the Call Stack. The environment is not garbage collected because the inner function still holds a pointer to it.
This is why a counter factory works: each call to `makeCounter()` creates a brand-new Lexical Environment, and the returned function closes over *that specific* environment — separate counters never share state.
Common closure pitfalls
The classic `for (var i ...)` loop bug happens because `var` is function-scoped, so all callbacks close over the *same* single `i` binding, which has finished looping by the time the callbacks run. `let` fixes this because it creates a fresh binding per loop iteration.
Closures keep their outer scope alive in memory — useful for encapsulation, but a source of memory leaks if you attach long-lived closures (e.g. event listeners) that hold references to large objects you meant to discard.
See It
Visualizations
Visualization
Scope chain as nested environments
Lookup walks outward: inner → outer → global, stopping at the first match.
Build It
Code Examples
A private counter via closure
function makeCounter() {
let count = 0; // lives in makeCounter's Lexical Environment
return {
increment() {
count += 1; // closes over 'count', not a copy of it
return count;
},
reset() {
count = 0;
return count;
},
};
}
const counterA = makeCounter();
const counterB = makeCounter();
counterA.increment(); // 1
counterA.increment(); // 2
counterB.increment(); // 1 <- independent environment, provenThe var-in-a-loop bug, and the let fix
// Bug: every callback closes over the SAME 'i'
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var i:', i), 0); // 3, 3, 3
}
// Fix #1: let creates a new binding per iteration
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let j:', j), 0); // 0, 1, 2
}
// Fix #2 (pre-ES6 idiom): force a new scope with an IIFE
for (var k = 0; k < 3; k++) {
(function (frozenK) {
setTimeout(() => console.log('iife k:', frozenK), 0); // 0, 1, 2
})(k);
}Remember
Key Takeaways
- Scope is resolved lexically — by where code is written, not by call order.
- A closure = a function + a live reference to the Lexical Environment it was created in.
- Each function call creates a brand-new environment; closures over different calls never collide.
- let/const in a for-loop create one binding per iteration; var creates one binding for the whole loop.
- Long-lived closures can leak memory by keeping large outer variables alive — release references you no longer need.
Do It
Practice
- 1Build a `makeBankAccount(balance)` closure exposing only `deposit`, `withdraw`, and `getBalance` — balance must be unreachable from outside.
- 2Rewrite the var-in-a-loop bug three ways: with let, with an IIFE, and with Array.prototype.forEach — verify all three log 0, 1, 2.
- 3Use Chrome DevTools → Sources → Scope panel to inspect the "Closure" entry while paused inside increment().