How do JavaScript closures work?

A closure is a function plus the scope it was created in. That sentence is correct and explains nothing until you watch two of them refuse to share a variable.

Two counters, one factory

function makeCounter() {
  let count = 0;
  return () => ++count;
}
const a = makeCounter(), b = makeCounter();
a(): 1 2 3
b(): 1

b starts at 1 after a has already reached 3. Each call to makeCounter() created a new count, and each returned function kept its own. They are the same code and different scopes.

The variable outlives the call

makeCounter has returned, yet: still here

Normally a local variable dies when the function returns. Here it does not, because the returned function still refers to it, so it stays reachable. That is the whole mechanism: not a copy of the value, but continued access to the binding.

Capture is by variable, not by value

let msg = "first";
const readIt = () => msg;
msg = "second";
readIt() after reassigning: second

The closure reads msg when it is called, not when it was created. This is the detail that turns the loop bug below from surprising into obvious.

The var-in-a-loop bug

for (var i = 0; i < 3; i++) fns.push(() => i);
with var : [ 3, 3, 3 ]
with let : [ 0, 1, 2 ]

var has one binding for the entire loop. All three functions closed over the same i, and by the time they ran the loop had finished and left it at 3. let gets a fresh binding per iteration, so each function has its own.

Through setTimeout the same thing, which is where most people meet it:

var:3 var:3 var:3 let:0 let:1 let:2

Before let existed the fix was to make a scope by hand:

for (var i = 0; i < 3; i++) (function (j) { fns.push(() => j); })(i);
with an IIFE : [ 0, 1, 2 ]

The argument j is a new variable per call, which is exactly what let now does for you.

Private state without a class

function account(start) {
  let balance = start;
  return { deposit: n => balance += n, get: () => balance };
}
acc.get() : 150
acc.balance : undefined

balance is not a property, so nothing outside can reach it. This was the standard way to get privacy in JavaScript for years, and it is still the shortest.

What a closure keeps alive

function leak() {
  const big = new Array(1_000_000).fill("x");
  return () => big.length;
}
the closure reports length 1000000 so the 1,000,000-item array cannot be freed

The returned function mentions only big.length — a number. The array stays anyway, because a closure retains its scope, not the subset of it that looks used. Keep such a function in a long-lived place, an event listener or a cache, and the array lives as long as the listener does.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).