RoadmapDay 2 / 80
JavaScript CoreMonth 1 · Week 1

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.

Mark this day complete

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.

Global Environment { makeCounter }
makeCounter() Environment { count: 0 }
increment() Environment { } → reads/writes outer count

Build It

Code Examples

A private counter via closure

js
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, proven

The var-in-a-loop bug, and the let fix

js
// 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

  1. 1Build a `makeBankAccount(balance)` closure exposing only `deposit`, `withdraw`, and `getBalance` — balance must be unreachable from outside.
  2. 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.
  3. 3Use Chrome DevTools → Sources → Scope panel to inspect the "Closure" entry while paused inside increment().