Closures
Mental model: A closure is a function that remembers the variables where it was written, not where it is called.
Level: intermediate · about 16 minutes
Closures are the concept most often described as hard and most often used without noticing. Build them in three steps and they stop being mysterious.
- Nest a function Nothing new yet: the inner function reads outward, exactly as lexical scope promises.
- Return the inner function instead of calling it Now
outerhas finished, yetinnerstill readssecret. The variable did not disappear. - Make it mutable and you have state Each call to
makeCountercreates a fresh, independentcount. Nothing outside can reach it.
It captures the variable, not the value
This is the detail that trips people up. A closure does not photograph the value at creation time; it holds on to the binding itself.
let message = 'first';
const show = () => console.log(message);
message = 'second';
show(); // 'second' — not 'first'
The interview classic
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}var i creates ONE binding for the whole loop. The callbacks all close over that same binding, and by the time the timers fire the loop has finished, so i is 3. Swap in let i and you get 0 1 2, because let creates a fresh binding for every iteration.
var — one shared binding
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// 3 3 3
let — a binding per iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// 0 1 2This is not a special case bolted on for loops. let in a for header is specified to create a new binding each time round, precisely so closures behave the way people expect.
Privacy, for free
function createAccount(initial = 0) {
let balance = initial; // genuinely private
return {
deposit(amount) { balance += amount; return balance; },
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
get balance() { return balance; },
};
}
const acct = createAccount(100);
acct.deposit(50);
console.log(acct.balance); // 150
console.log(acct.__balance); // undefined — there is no back door
Try it yourself
Counter with private state
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
get value() { return count; },
};
}
const c = makeCounter();
c.increment();
c.increment();
c.decrement();
console.log(c.value); // 1
console.log(c.count); // undefined — private
Add a reset() method. Then add a step parameter so makeCounter(5) counts 5, 10, 15.
The loop puzzle
console.log('--- var ---');
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i));
}
console.log('--- let ---');
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j));
}
Fix the var version so it logs 0, 1, 2 — WITHOUT changing var to let. There are at least two ways.
Exercises
Make an ID generator
Write makeIdGenerator() which returns a function. Each call to that function returns the next id as a string: "id-1", "id-2", and so on. Two generators must not share a counter.
Fix the loop without let
Rewrite logIndexes so it logs 0, then 1, then 2 — but you may not use let or const for the loop counter. Use a closure to capture each value. logIndexes should return an array of the values that were logged, so the tests can check it.
Check yourself
- What does this log?
- 3 1 — Each call to
make()creates a new scope with its ownn.ahas been called three times so it returns 3;bis on its first call so it returns 1. Independent closures over independent bindings. - A closure captures…
- the variables themselves, so later changes are visible — It captures the binding, not a snapshot. That is exactly why the
varloop logs 3 three times: the value changed after the functions were created, and they all see the change. - Why does
for (let i …)behave differently fromfor (var i …)withsetTimeout? letcreates a new binding for each iteration — Aletdeclaration in aforheader is specified to create a fresh binding per iteration, so each callback closes over its own copy.varhas exactly one binding for the whole loop.
Common mistakes
- Thinking the closure stores a copy of the value. It holds the variable.
- Creating closures inside a hot loop and wondering where the memory went.
- Assuming
thisfollows closure rules. It does not — see thethislesson.
Takeaways
- A closure is a function plus the scope it was created in.
- It captures variables, not values, so later changes are visible.
letin a loop header creates one binding per iteration;varcreates one in total.- Closures are how JavaScript had private state long before
#privatefields existed.