6. Closures: functions that remember
A function carries the place where it was written along with it. That lets it keep private memory between calls, and it is exactly how the interpreter you build later will handle variables.
- Say where a name declared with let or const can be seen, and predict a ReferenceError.
- Explain lexical scope: a function sees the names around where it was written, not where it is called.
- Use a closure to give a function private state that survives between calls.
- Explain why two closures made by two calls don't share their variables.
A counter anyone can break
You want a function that hands out ticket numbers: 1, then 2, then 3. It has to remember the last number between calls, so you keep it outside:
let count = 0;
function next() {
count = count + 1;
return count;
}
next(); // 1
next(); // 2
count = 500; // any code can do this
next(); // 501
Two problems. Any line of the program can change count by accident.
And there’s only one counter: a second ticket queue would need a second
variable and a second function. A local variable would be private, but
chapter 1 said it’s thrown away when the call ends, so it can’t remember.
You want both: private and remembered. To get there, you first need to
know exactly where a name can be seen.
Scope: where a name is visible
Picture rooms inside rooms, with one-way glass. From inside a room you can see out into every room that surrounds it. From outside, you can’t see in. Names written in a room belong to that room.
function price(n) {
if (n > 9) {
const extra = 1;
console.log(n + extra); // fine
}
console.log(extra);
// ReferenceError: extra is not defined
}
extra lives in the if block’s braces. Once those braces close, the name
is gone. The same goes for the i in for (let i = 0; ...): it only exists
inside that loop.
The scope of a name is the region of code where it can be used.
let and const have block scope: a name is visible from where it is
declared to the end of the nearest enclosing { }. Code can see the names
of every scope around it, but not the names inside a scope it contains. If
an inner scope declares the same name as an outer one, the inner one wins
inside it.
Functions inside functions
A function is a value, so you can create one inside another function, just as you’d create a number there. It is then a local name of the outer function, and it can see the outer function’s names:
function greetAll(names, hi) {
const greet = (name) => {
console.log(hi + ', ' + name);
};
for (let i = 0; i < names.length; i++) {
greet(names[i]);
}
}
greetAll(['Ada', 'Alan'], 'Hello');
// Hello, Ada
// Hello, Alan
greet never receives hi as an argument. It just uses it, because it
was written inside the room where hi lives. The rule is about where
code is written, not where it is called from:
const x = 'outer';
function show() {
console.log(x);
}
function run() {
const x = 'inner';
show();
}
run(); // outer
show is called from inside run, but show was written out in the
program, and the only x it can see from there is 'outer'.
A nested function is a function defined inside another function. JavaScript uses lexical scope: the names a function can see are decided by where it is written in the source code, not by where it is called[1]. (“Lexical” means “to do with the text of the program”.)
Closures
Now put the pieces together. Here is the counter again, with count
moved inside a function:
function makeCounter() {
let count = 0;
return () => {
count = count + 1;
return count;
};
}
const a = makeCounter();
console.log(a()); // 1
console.log(a()); // 2
By chapter 1’s rule, makeCounter’s page should vanish when it returns.
But the arrow function it returns still uses count. Step through to see
what really happens:
function makeCounter() {let count = 0;return () => {count = count + 1;return count;};}const a = makeCounter();a();a();const b = makeCounter();b();
So chapter 1’s rule needs one correction. A call’s page is thrown away only once nothing points at it any more. A function made inside the call points at it, so as long as that function is alive, the page is too.
A closure is a function together with the variables from the place it
was written. In JavaScript, every function forms a closure when it is
created[1]. The captured variables live as long as
the function does, and code outside can reach them only through it. Each call of
makeCounter makes a new page, so each closure gets its own private
count.
This also answers last chapter’s puzzle: times(3) returned a function
that closed over the page where k is 3.
A closure holds on to the variable, not a snapshot of its value. If the variable changes, the closure sees the new value:
let n = 1;
const getN = () => n;
n = 2;
console.log(getN()); // 2
function makeCounter() {
let count = 0;
return () => {
count = count + 1;
return count;
};
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), a());
console.log(b());
console.log(a());Closures in a loop
Block scope and closures meet in one common situation. Each pass of a
for (let i ...) loop gets its own fresh i, so a function made in that
pass closes over that pass’s i:
const fs = [];
for (let i = 0; i < 3; i++) {
fs.push(() => i);
}
console.log(fs[0](), fs[2]()); // 0 2
Three functions, three different is. (Older JavaScript code uses var
instead of let. A var isn’t block-scoped, so all three functions
would share one variable and every call would give 3. That’s one reason
this book uses only let and const.)
You’ll build all of this. In chapter 37 your interpreter gets an environment: a table from names to values, plus an arrow to the enclosing environment. That is the rooms picture, made of data. Looking a name up means checking this table, then its parent, and so on outward. In chapter 41, a function value will store the environment it was created in, which is exactly the closure arrow in the step-through above. And in chapter 57, the virtual machine will have to move captured variables out of a call’s short-lived storage when the call returns, because a closure still needs them.
Viscosity cares too. If a closure captures your particles array, then every later call of that closure might read or change the particles. Before it changes their layout, Viscosity has to find every closure that can reach them.
Your turn
Write makeTotal(). It returns a function add(n) that adds n to a running total and returns the new total. The total starts at 0, is private, and every call of makeTotal gets its own. So after const add = makeTotal();, add(5) is 5 and then add(3) is 8.
function makeTotal() {
// your code here
}
const add = makeTotal();
console.log(add(5));
console.log(add(3));Write once(f). It returns a new function. The first time that function is called, it calls f() and returns its result. Every later call returns that same result without calling f again. f must not be called before the first call.
function once(f) {
// your code here
}
const hi = once(() => {
console.log('working...');
return 42;
});
console.log(hi());
console.log(hi());What you learned
- The scope of a name is where it can be used.
letandconstare block-scoped: they live until the closing}. - Inner code sees outer names; outer code never sees inner ones.
- Lexical scope: what a function can see is decided by where it is written, not where it is called.
- A nested function can use the names of the function around it.
- A closure is a function plus the variables it captured. They outlive the call that made them, stay private, and are shared live, not copied.
- Each call makes a new page, so each closure gets its own variables.
Next: classes, a built-in way to make many objects of the same shape and give them shared behaviour.
Check your understanding
0/6- 1.
What happens?
if (true) { const z = 1; } console.log(z); - 2.
What does this print?
const x = 'outer'; function show() { console.log(x); } function run() { const x = 'inner'; show(); } run(); - 3.
const a = makeCounter(); const b = makeCounter();Thena(); a(); a();. What doesb()return? - 4.
What does this print?
let n = 1; const f = () => n * 10; n = 5; console.log(f()); - 5.
Which best describes a closure?
- 6.
Why is
makeCounter’scountsafe from the rest of the program?
References
- [1] MDN contributors. Closures (JavaScript Guide). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures (checked 2026-10-31)