JavaScript closures and scope

Use a function that remembers its surrounding variables, and see why let fixes the classic loop bug.

JavaScript
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(","));
Output
2 1
3,3,3
0,1,2

Explanation

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.

Explore the dry run
Guided walkthroughReady
1function makeCounter() {
2 let count = 0;
3 return {
4 increment: () => ++count,
5 value: () => count,
6 };
7}
8
9const a = makeCounter();
10const b = makeCounter();
11a.increment();
12a.increment();
13b.increment();
14console.log(a.value(), b.value());
15
16const withVar = [];
17for (var i = 0; i < 3; i++) {
18 withVar.push(() => i);
19}
20console.log(withVar.map((f) => f()).join(","));
21
22const withLet = [];
23for (let j = 0; j < 3; j++) {
24 withLet.push(() => j);
25}
26console.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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

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.

Further reading