JavaScript closures and scope
Use a function that remembers its surrounding variables, and see why let fixes the classic loop bug.
On this page
JavaScript functions and arrow functions.
- Use a function that remembers its surrounding variables, and see why let fixes the classic loop bug.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A closure is a function together with the variables from the scope where it was created. makeCounter creates a local variable count and returns functions that use it. Each call to makeCounter creates a separate count, so two counters never interfere. The variable is private: only the returned functions can reach it.
var is function-scoped, so all the closures created in a for loop with var share one variable, and by the time they run it has its final value. let is block-scoped, and each loop iteration gets a fresh binding, so each closure sees its own value. Prefer let and const in new code.
Code example
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
value: () => count,
};
}
const a = makeCounter();
const b = makeCounter();
a.increment();
a.increment();
b.increment();
console.log(a.value(), b.value());
const withVar = [];
for (var i = 0; i < 3; i++) {
withVar.push(() => i);
}
console.log(withVar.map((f) => f()).join(","));
const withLet = [];
for (let j = 0; j < 3; j++) {
withLet.push(() => j);
}
console.log(withLet.map((f) => f()).join(","));2 1 3,3,3 0,1,2
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
function makeCounter() { let count = 0; return { increment: () => ++count, value: () => count, };} const a = makeCounter();const b = makeCounter();a.increment();a.increment();b.increment();console.log(a.value(), b.value()); const withVar = [];for (var i = 0; i < 3; i++) { withVar.push(() => i);}console.log(withVar.map((f) => f()).join(",")); const withLet = [];for (let j = 0; j < 3; j++) { withLet.push(() => j);}console.log(withLet.map((f) => f()).join(","));Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| count lives in the scope of one makeCounter call and survives after the call returns because the returned functions use it. | — | — | |
| A second call creates a second, independent count. | — | — | |
| a was incremented twice and b once. | — | 2 1 | |
| var gives the whole loop one shared variable i. | — | 2 1 | |
| By the time the closures run, the loop has finished and i is 3 for all of them. | — | 2 1 3,3,3 | |
| With let, each iteration has its own j, so the closures return 0, 1, and 2. | — | 2 1 3,3,3 0,1,2 |
Behind the scenes
How JavaScript processes this code
A JavaScript engine parses and runs the source. A function body runs when the function is called. console.log writes to the console.
Cost and common mistakes
Creating a closure is O(1); calling its functions is O(1) here.
- A closure keeps its variables alive as long as the function exists. Long-lived closures that capture large objects can cause memory leaks.
- Closures capture variables, not values. If the variable changes later, the closure sees the new value.
- Using var also hoists the declaration to the top of the function, which hides mistakes. let and const avoid that.
Try it yourself
What does a.increment() return the first time?
Reveal answer and reasoning
1, because ++count increments first and then returns the new value.