codejump
Academy

Closures in JavaScript

What a closure is, why the variable outlives the function that created it, and the loop bug every JavaScript developer hits once.

Updated

What a closure is

A closure is a function that keeps access to the variables around it at the place it was written, and keeps that access after the surrounding function has already returned.

That second half is the whole idea. A function remembering its own arguments is unremarkable. A function remembering variables belonging to a call that finished ten minutes ago is what makes closures worth a name.

function makeCounter() {
  let count = 0;              // local to this call of makeCounter
  return () => ++count;       // ...and still reachable from here
}

const next = makeCounter();
next(); // 1
next(); // 2

makeCounter returned long ago. Its count did not disappear, because the returned arrow function still refers to it, and a variable that something still refers to cannot be collected. count is now reachable through exactly one door — calling next — and through no other. Nothing outside can read it, assign to it, or even name it.

Why this is the closest thing JavaScript has to a private field

Before #private class fields, this was *the* way to make state that callers cannot corrupt:

function createAccount(initial) {
  let balance = initial;
  return {
    deposit: (n) => { balance += n; },
    getBalance: () => balance,
  };
}

const acc = createAccount(100);
acc.deposit(50);
acc.getBalance(); // 150
acc.balance;      // undefined — there is no such property

Both methods close over the same balance. That is the part people get wrong when they first meet closures: a closure does not copy the variable, it keeps a reference to the binding. Two functions created in the same scope share one variable, not two snapshots of it.

The loop bug

This is the closure question asked in interviews, and the reason let was given per-iteration scoping:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

There is only one i. var scopes it to the whole function, all three callbacks close over that single binding, and by the time the timers fire the loop has finished and left it at 3.

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0, 1, 2

let creates a new binding per iteration, so each callback closes over its own. The fix is not that let is "safer" in some vague way — it is that the number of variables changed from one to three.

Where closures cost you

A closure keeps its whole scope alive, not just the parts it uses. The engine is allowed to optimise away what is provably unreachable, but you should not plan around that:

function attach(element) {
  const huge = new Array(1e6).fill('...');  // never used below
  element.onclick = () => console.log('clicked');
}

The handler closes over attach's scope. As long as the element lives, that scope may live, and huge with it. This is a common shape of leak in long-lived pages: a listener that is never removed, holding a scope that holds a large object.

The rule that avoids it is simple — create the closure in the smallest scope that contains what it actually needs.

How to recognise one in the wild

Once you see the pattern, closures turn out to be most of the utilities you already use:

UtilityWhat the closure holds
debouncethe pending timer id
throttlethe timestamp of the last call
memoizethe cache
oncewhether it has run, and the first result
a curried functionthe arguments received so far

None of these need a class. Each is a function that returns a function, over a variable the caller can never touch.

Now practice it

Reading this page is the cheap half. These are the exercises that make you use it.