20 · 10 steps
Visualize / 20

Closures and lexical scope.

A function that references a variable from its surrounding scope is a closure. The runtime keeps that scope alive — even after the outer function returns — so the inner function can still read and write it. Watch makeCounter build two independent counters that share nothing.


step 1 / 10
SOURCE 1 function makeCounter() { 2 let count = 0; 3 return function() { 4 count += 1; 5 return count; 6 }; 7 } 8 9 const a = makeCounter(); 10 const b = makeCounter(); 11 a(); // 1 12 a(); // 2 13 b(); // 1 14 a(); // 3SCOPES · active framesFRAME · globalCLOSURES · live environmentsCONSOLEEVENTenterclosures kept alive: 0
About to call makeCounter() for a

global scope so far has no bindings. We're about to call makeCounter and capture its return value into a.

Why the count survives the return

Without closures, count would be a stack local — gone the moment makeCounter returns. With closures, the runtime detects that an inner function references count, so it allocates count on the heap (as part of an environment object) and gives the closure a reference to that environment. The function value isn\'t just bytecode; it\'s a pair: (code pointer, environment reference). GC keeps the environment alive as long as the closure is reachable.

The classic for (var i ...) gotcha

In old-style JS: for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0) prints 3, 3, 3 — not 0, 1, 2. The reason: var is function-scoped, so all three callbacks capture the SAME i, which is 3 by the time they run. let fixes it by creating a fresh i per iteration; each closure captures its own. The same lesson plays out in Python, Go, Java lambdas — any language where lexical scope and async closures collide.

Closures vs objects

A closure that captures one variable plus the function that mutates it is logically equivalent to an object with one field and one method. makeCounter is a constructor; the returned function is the only method. This equivalence runs deep — functional programmers say "objects are a poor man\'s closures" and OO programmers say the reverse. In practice both are fine; closures are lighter syntax for single-method "objects," classes are clearer when you have many methods sharing state.

Go deeper

Lexical scope across languages →

How closures are implemented in JavaScript, Python, Rust, Go, Java. Why some languages need explicit capture syntax. Memory implications of long-lived closures.

Open the Codex →
Found this useful?