JavaScript Closures Table
| Piece | What it does | Field note |
|---|---|---|
function outer() { let x = 1; return function() { return x; } } | The closure form | The inner function keeps x alive after outer returns |
let make = outer(); make() | Invoke the kept state | x survives - the frame is retained, not copied |
function counter() { let n = 0; return () => ++n; } | Private state | n is invisible from outside - the pre-class privacy |
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) | The var loop bug | Prints 3,3,3 - var is one shared binding |
for (let i = 0; ...) setTimeout(...) | Prints 0,1,2 | let makes a fresh binding per iteration |
IIFE capture | Per-iteration capture (old fix) | (function(j) { ... })(i) - the pre-let workaround |
A closure is a function bundled with the variables it references from where it was defined - those variables stay alive as long as the function does, invisible from outside. The table below is the working six: the basic form, the counter as private state, the var loop bug everyone meets once, and the fixes in historical order.
Bottom line: closures are how JavaScript makes private state without classes - a counter's count cannot be read or reset except through the returned function. And they are already everywhere: every callback that touches an outer variable is a closure working. If your setTimeout in a loop prints the final value three times, that is a closure working TOO - with var's one shared binding; let fixes it by making a fresh binding per iteration.
The honest part: the mental model beats memorization. Functions remember the BINDING, not the value - so closures see updates (two closures sharing a counter see each other's increments). And closures hold memory: everything the captured scope references stays alive, which is why long-lived handlers capturing large objects are a leak pattern. Capture deliberately, drop listeners when done.
How to use
- Spot the closure: any function that reads a variable from an outer scope that has already returned.
- Use the counter pattern for private state that only your methods can touch - the pre-class answer that still fits small jobs.
- Fix loop-and-timeout bugs with let (one binding per iteration); recognize the IIFE version as the pre-let museum piece.
Frequently asked questions
What exactly is a closure in JavaScript?
A function plus the variables from its defining scope that it keeps referencing. When the outer function returns, its local variables would normally die - but if the returned function references them, the engine keeps that scope alive. The inner function carries its birthplace with it: wherever you call it later, the variables it closed over are the ones from its creation, updated with any changes made since.
How do closures create private state?
The counter pattern: function counter() { let n = 0; return { inc: () => ++n, get: () => n }; } - n exists only inside the closure. Nothing outside can read it directly, reset it, or even confirm its type; the returned methods are the only doors. Before #private fields, this was THE privacy mechanism, and it still fits when the state is small and one factory function is cleaner than a class.
Why does setTimeout in a loop print the same value three times?
Because var declares ONE variable for the whole function - the loop's three timeouts all close over that single binding, and by the time they run, the loop has finished and the binding holds 3. let in the loop header creates a fresh binding per iteration, so each callback closes over its own copy and prints 0, 1, 2. The pre-let workaround wraps each iteration in an IIFE that captures the current value as a parameter - the museum piece that explains old code.
Do closures cause memory leaks?
They can hold memory deliberately, which becomes a leak when the held data is huge and the closure lives long. A closure keeps its entire defining scope reachable - capture one variable and the engine may retain the whole scope object, including the 2MB buffer next to it. The discipline: in long-lived handlers (event listeners on document, intervals), capture only what is needed, drop listeners with removeEventListener or AbortSignal when done, and null out large references the closure no longer needs.