Kodokon kodokon.com

Closures and lexical scope

Master how closures capture variables so you can encapsulate state and avoid scoping pitfalls.

9 min · 3 questions

Open this lesson in Kodokon

A closure is not some exotic trick: it is the mechanism by which a function keeps access to the lexical scope where it was defined, not the one where it is called. The engine does not copy values: it keeps a live reference to the lexical environment. Two calls to the same factory therefore produce two independent environments, which makes closures the most lightweight encapsulation tool in the language.

JAVASCRIPT
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => { count = 0; },
  };
}
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
console.log(counterA.increment()); // 3
console.log(counterB.increment()); // 1
Private state: count is only reachable through the returned methods.

The classic real-world trap: var is bound to the enclosing function, not to the block. Inside a loop, every closure then captures the same variable. With let, each iteration creates a fresh lexical environment, so each capture is distinct. Add to that the temporal dead zone (TDZ): a let or const variable exists as soon as the block is entered but throws a ReferenceError if you read it before its declaration.

JAVASCRIPT
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
var shares a single binding; let creates one per iteration.

Pattern-wise, closures are the foundation of memoization and the module pattern. Compared to a class, a closure gives you genuine privacy without this or prototypes, at the cost of slightly higher memory usage when you create thousands of instances: each instance carries its own functions.

JAVASCRIPT
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}
const square = (n) => n * n;
const fastSquare = memoize(square);
console.log(fastSquare(9)); // computed
console.log(fastSquare(9)); // read from the cache
The cache lives inside the closure, invisible from the outside.

Knowledge check

Make sure you remember the key points of this lesson.

  1. In a for (var i = ...) loop, what do three setTimeout callbacks created inside the loop body log?
    • 0, 1, 2 because each callback captures its own value
    • 3, 3, 3 because they all share the same binding of i
    • undefined three times because i is destroyed after the loop
  2. What exactly does a closure keep from the environment where it was defined?
    • A frozen copy of the values at creation time
    • A live reference to the captured variables
    • Only the parameters of the enclosing function
    • Nothing: the scope is destroyed when the function returns
  3. Why does createCounter guarantee that count stays private?
    • Because let makes the variable immutable
    • Because count is only reachable through the returned functions, the only ones that capture its scope
    • Because the engine deletes count after the factory returns