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, ...).