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.

  1. Nest a function Nothing new yet: the inner function reads outward, exactly as lexical scope promises.
  2. Return the inner function instead of calling it Now outer has finished, yet inner still reads secret. The variable did not disappear.
  3. Make it mutable and you have state Each call to makeCounter creates a fresh, independent count. 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 2

This 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 own n. a has been called three times so it returns 3; b is 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 var loop 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 from for (var i …) with setTimeout?
let creates a new binding for each iteration — A let declaration in a for header is specified to create a fresh binding per iteration, so each callback closes over its own copy. var has 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 this follows closure rules. It does not — see the this lesson.

Takeaways

  • A closure is a function plus the scope it was created in.
  • It captures variables, not values, so later changes are visible.
  • let in a loop header creates one binding per iteration; var creates one in total.
  • Closures are how JavaScript had private state long before #private fields existed.