Some functions carry a memory with them. Declared inside another function, a function keeps seeing its neighbor's variables long after that neighbor has finished running, as if it took a piece of the scenery along.
The mechanism has a name, the closure, and it explains much of what feels mysterious in JavaScript: counters that hold on to their value, genuinely private data without a class, and callbacks that print an unexpected number.
Definition
The counters come out of the same factory, yet their values are 6 apart. Each one keeps its own total variable, alive long after createCounter() returned.
Each row shows the value of the total variable that this counter reads, not the last value it returned: that is what makes a never-called counter visible. Switching versions replays the same sequence of calls against the other code without resetting anything; in a real program you would have to reload the page to change implementation. The bars are scaled to 12 calls, then to the total once it goes past 12, and the exact values are printed next to them.
Create several counters and call them. Each keeps its own count although they all come from the same code. Then switch to the shared variable to see the difference.
A closure is the pairing of a function with the environment in which it was declared. As long as the function exists somewhere, the variables of that environment stay alive, even though the code that created them has finished.
function createCounter() {
let total = 0;
return function () {
total += 1;
return total;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(createCounter()()); // 1, a separate environmentThe total variable should vanish when createCounter() returns. It survives because the returned function still references it. And every call to createCounter() builds a brand new environment: two counters never step on each other.
What it buys you in practice
- Private state.
totalis reachable by no other code: no reading, no writing, no deleting. - A function factory. A function that returns another one, preconfigured with the values it received.
- Memory between two calls. That is exactly how Memoization works, keeping its cache inside the closure.
- An isolated module. Historically the job of the IIFE (immediately invoked function expression), before modules took over.
function multiplyBy(factor) {
return (value) => value * factor;
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(7)); // 14
console.log(triple(7)); // 21The loop trap
This is the most common closure-related bug, and it comes down entirely to the Scope of the loop variable.
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("var", i), 0);
}
// var 4, var 4, var 4
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log("let", j), 0);
}
// let 1, let 2, let 3With var there is a single variable for the whole loop: the three functions share it and read its final value. With let, every iteration gets its own variable, and therefore its own closure.
This trap is the most concrete reason to prefer let over var inside a loop, well ahead of any style argument.
Frequently asked questions
Does a closure keep the whole memory of the parent function?
No, modern engines only retain the variables the inner function actually references. Everything else is released as usual. Memory leaks happen when a closure lives a long time, for instance attached to a listener that is never removed, and holds a large object along the way.
How is this different from an object holding the same data?
An object exposes its properties: any code can read and overwrite them. A closure only lets through what you explicitly return. It is the one truly airtight form of encapsulation in the language, alongside the private fields of a class written with a hash sign.
Should closures be avoided for performance reasons?
No. Creating a closure costs roughly as much as creating an object, which is negligible in the vast majority of cases. The only scenario worth watching is creating thousands of functions inside a tight loop. Our JavaScript course comes back to this mechanism when covering callbacks, where it becomes unavoidable.