Topic 3.6
Closures
In one line
A closure is a function together with the variables of the place where it was created; the function keeps using those variables even after the outer function has finished, which gives you counters, private state and function factories, and explains the classic var loop bug.
Think of it like this
A school backpack. When a child leaves home in the morning, they carry a backpack packed at home: lunch, a pencil case, a note from their parents. At school, far from home, they can still open the backpack and use what's inside, and even add or eat things. A closure is a function carrying a backpack of the variables from the place it was made. Each child packed at a different time gets their own backpack.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Closure
- A function plus the variables from the place it was created, which it keeps using later.
- Inner function
- A function defined inside another function.
- Outer function
- The function that contains the inner function's definition.
- Capture
- When an inner function uses an outer variable, it captures it: it keeps a live link to it.
- Function factory
- A function that builds and returns new functions, each with its own captured variables.
- Private state
- Data that only certain functions can read or change, because no outside name reaches it.
- Encapsulation
- Hiding data and allowing changes only through a few chosen functions.
- Binding
- The link between a name and its storage.
letin aforloop makes a new one each iteration.
Step by step
01Write a function that returns a function
makeCounter declares a local count, then returns an inner function that uses it.
function makeCounter() {
let count = 0;
return function () {
count = count + 1;
return count;
};
}02Call the outer function once
const next = makeCounter(); runs makeCounter to the end. Normally count would vanish now. But the returned function uses it, so count is kept alive, attached to next.
03Call the closure several times
Each call reads and updates the same remembered count. The value survives between calls.
const next = makeCounter();
next(); // 1
next(); // 2
next(); // 304Make a second counter
Calling the factory again creates a brand-new count. The two closures never share.
const other = makeCounter();
other(); // 1, not 405Try to reach the private variable
From outside, count doesn't exist: typeof count is "undefined" and next.count is undefined. The only door in is the function you returned.
06Understand the var loop bug
With var, all loop callbacks share one i. With let, each iteration gets its own i. Same code, one keyword different.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10); // 3 3 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 10); // 0 1 2
}Try it yourself
- 1
Add a reset
In the first example, change
makeCounterto return an object{ next, reset }, whereresetsetscountback to 0. Predict: does resettingaaffectb? - 2
Add a rule
In the wallet example, make
spendrefuse amounts of 0 or less with the message"Amount must be positive". Can outside code skip your rule? Try. - 3
Fix the var loop without let
In the third example, keep
var kbut wrap thesetTimeoutin(function (saved) { setTimeout(() => console.log("var k:", saved), 10); })(k);. Predict the new output.
Code & diagrams
`a` and `b` each carry their own `count`. Nothing outside can reach it.
Console
(nothing was printed)Expected console output
1 2 3
1
undefined undefined
10 15The buttons are closures too: each arrow remembers `wallet`. Click Spend until the rule stops you.
Preview
Console
Nothing logged yet.Expected console output
150
Not enough money
120
undefined
120All the `var` callbacks share one `i` (or `k`), which is 3 when they run. `let` gives every iteration its own variable.
Console
(nothing was printed)Expected console output
[ 3, 3, 3 ]
[ 0, 1, 2 ]
var k: 3
var k: 3
var k: 3
let m: 0
let m: 1
let m: 2Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Create the counter inside the call
Write console.log(makeCounter()(), makeCounter()(), makeCounter()()).
Break #2
Loop handlers all point at the last item
Attach click handlers in for (var i = 0; i < buttons.length; i++) { buttons[i].onclick = () => show(i); }.
Myth vs fact
Myth
A closure copies the variable's value when it's created.
Fact
It keeps a live link to the variable itself. Later changes are visible, and the closure can change it too.
Myth
Closures are an advanced feature you create on purpose.
Fact
Every function that uses an outer variable is a closure. You use them with every callback you write.
Myth
Closures cause memory leaks.
Fact
They keep captured variables alive only while the closure itself is reachable. Leaks happen when a closure is kept forever by mistake, such as a listener that's never removed.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Engines don't keep the whole outer scope alive: V8 analyses which variables inner functions actually use and moves only those into a heap-allocated Context; the rest stay on the stack and disappear. But all closures created in the same scope share one Context, so a tiny closure can keep a big variable alive if a sibling closure captured it.
- ▸
The per-iteration
letbinding inforloops is specified inCreatePerIterationEnvironment: each iteration copies the current value into a fresh binding before the update expression runs. - ▸
In DevTools, pause inside a function and look at the Scope pane: the Closure section lists exactly which outer variables that function captured. Heap snapshots show closures as
(closure)entries with theircontext. - ▸
Classes with
#privatefields (ES2022) now offer true privacy with methods shared on the prototype; closures give privacy per object but create new method functions for every instance. Both are valid; pick by how many instances you create.
Remember this
- 1
Normally, when a function finishes, its local variables are thrown away. But if the function creates an inner function that uses those variables, and that inner function survives (it's returned, stored, or used as a callback), the variables can't be thrown away: the inner function still needs them. The inner function plus the variables it remembers is called a closure. You don't do anything special to make one: every JavaScript function remembers the scope it was created in (that's lexical scope, Topic 3.4).
- 2
The remembered variables are live, not a snapshot. The closure holds a reference to the variable itself, so it reads the current value, and if it changes the variable, the change sticks for the next call. That's how a counter works:
function makeCounter() { let count = 0; return () => ++count; }. Each call to the returned function bumps the samecount. - 3
Each call to the outer function creates a fresh set of variables, so each returned function has its own backpack.
const a = makeCounter(); const b = makeCounter();gives two independent counters:a()moving to 3 doesn't touchb. This pattern of a function that builds and returns new functions is called a function factory. - 4
Closures give you private state. Code outside
makeCounterhas no way to reachcountdirectly: no name refers to it from outside. The only way to change it is through the functions you returned, so you can enforce rules ("never go below zero", "only positive deposits"). This is called encapsulation, and it's how JavaScript had private data long before classes got#privatefields (ES2022). - 5
The famous **loop +
varbug**:for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 10); }prints3 3 3, not0 1 2. There is only onevar ifor the whole function, and all three callbacks close over that same variable; by the time they run, the loop has finished andiis3. Withlet, the spec creates a **new binding ofifor every iteration**, so each callback closes over its own copy:0 1 2. Beforeletexisted, people fixed it with an IIFE that tookias a parameter (Topic 3.9). - 6
Closures are everywhere once you look: event handlers that remember which item they belong to,
setTimeoutcallbacks,once,debounceandmemoizehelpers (Topic 3.7), React hooks and module patterns. The cost to keep in mind: everything a closure can reach stays in memory as long as the closure is alive, so a long-lived closure (like an event listener never removed) that captures a large object keeps that object alive too.
Explain it without notes
What is a closure? Explain with a counter example.
Why do two counters made by the same factory not share their count?
Explain why a for loop with var and setTimeout prints the same number three times, and two ways to fix it.
How do closures provide private state, and what is a downside?
Practice
Write makeGreeter(greeting) that returns a function taking a name, so makeGreeter("Hi")("Asha") returns "Hi, Asha".
Write makeCounter(start = 0) that returns { increment, decrement, value } where value() returns the current count.
Write once(fn) that returns a function which calls fn only the first time and returns the same result on every later call.
Trade-offs
- ↔
Closure-based privacy is simple and truly private, but each object gets its own copies of the functions;
classwith#privatefields shares methods and is easier to inspect. - ↔
Long-lived closures (listeners, caches) are convenient but keep everything they capture in memory; remove listeners and clear caches you no longer need.
Done when you can
I can define a closure in one sentence and point to one in code.
I can build a counter and a function factory with closures.
I know captured variables are live, not copies.
I can explain and fix the loop +
varbug.I can use a closure to keep state private behind a few functions.