A closure is a function together with the variables from the place where it was created. The function keeps access to those variables even after the outer function has returned. Every JavaScript function is a closure; it only becomes visible when a function is used outside the place it was written.
Try it
This counter remembers its count between calls. Press Next step and watch the Scope and variables panel: when next() runs, it still sees makeCounter's count, although makeCounter has finished.
Code (current line highlighted)
Call stack (top first)The function running right now is on top. Calling a function adds a frame; returning removes it. Only one frame runs at a time.
Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.
Microtask queue (next first)Promise callbacks (.then, code after await, queueMicrotask) wait here. Once the call stack is empty, all of them run before the next task.
Task queue (next first)Timer callbacks wait here. The event loop takes one task, runs it, then empties the microtask queue before taking the next.
Console output
Scope and variables (yellow = changed this step)
Timeline (click a block or use the arrow keys)Script, function calls and returnsawait and microtasksTimers and tasksConsole outputErrors
Step by step: what the example does
function makeCounter() {
let count = 0;
return function () {
count = count + 1;
return count;
};
}
const next = makeCounter();
console.log(next());
console.log(next());
const other = makeCounter();
console.log(other());makeCounter()runs and creates a local variablecount = 0.- It returns the inner function without calling it. That function was created inside
makeCounter, so it keeps a link tomakeCounter's variables. makeCounter's frame leaves the call stack, butcountis not thrown away: the returned function still refers to it.- Each
next()call adds 1 to that samecount, so the output is 1, then 2. makeCounter()is called again forother. This creates a brand-newcount, soother()prints 1. The two counters never share a variable.
Lexical scope: where a function is written matters
JavaScript decides which variables a function can see from where the function is written in the code, not from where it is called. This is called lexical scope. When a function looks up a name, it checks its own variables first, then the scope it was created in, then that scope's parent, up to the global scope. A closure is simply that chain of scopes staying available for as long as the function exists. The execution context page explains how these scopes are created.
Each call creates a new set of variables
Calling the outer function again doesn't reuse the old variables; it makes new ones. That is what makes closures useful for private state: nothing outside the counter can read or change count except through the function that closes over it.
function once(fn) {
let done = false;
return function (...args) {
if (done) return;
done = true;
return fn(...args);
};
}
const init = once(() => console.log("set up"));
init(); // set up
init(); // (nothing)Here every function made by once gets its own done flag. Event handlers, timer callbacks and promise callbacks use closures in the same way to remember values from the code that created them.
The classic loop bug: var vs let
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var", i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let", j), 0);
}This prints var 3 three times, then let 0, let 1, let 2. A var variable belongs to the whole function (or script), so all three callbacks close over the same i, and by the time the timers run, the loop has already set it to 3. A let in a for loop gets a fresh copy for every round, so each callback remembers its own j. To step through it, choose var vs let in a loop in the example menu above.
Closures and memory
Variables that a closure uses stay in memory for as long as the closure can still be called. That is usually what you want. It only becomes a problem when a long-lived function, such as an event listener that is never removed, keeps large data alive by accident. Removing the listener, or not referring to the data inside it, lets the engine free it.
Common mistakes
- Expecting a closure to copy a value. It keeps the variable itself, so it sees later changes, as in the
varloop. - Thinking closures need special syntax. Any function that uses a variable from an outer scope is a closure.
- Assuming two calls share state. Each call of the outer function makes its own variables, like the two counters above.
What LoopLens simulates
Scopes and closures are real in LoopLens: its interpreter creates a new scope for every call and keeps it alive while a function refers to it, exactly as described here. The Scope panel lists the scopes the current code can see, innermost first. The call stack, queues and clock are simulated.