Viscosity
Review
Part I · JavaScript for engine builders · Day 2

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.

About 17 minutes, including the exercises and quiz
By the end you will be able to
  • 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

Intuition

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.

Three nested scopes. The whole program holds rate and price. Inside it, the function price holds n. Inside that, an if block holds extra. Code in an inner room can see the names of every room around it, but not the other way round.THE PROGRAMconst rate = 2;INSIDE function price(n)nINSIDE if (n > 9) { }const extra = 1;sees extra, n, ratelooks outward:sees n, ratenever extra
Inner code sees outward, never inward. The if block sits inside the function, which sits inside the program. Code in the block can use extra, n and rate. Code in the function can use n and rate, but not extra.
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.

Definition

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'.

Definition

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:

Step throughA function that keeps its page
function makeCounter() {
let count = 0;
return () => {
count = count + 1;
return count;
};
}
const a = makeCounter();
a();
a();
const b = makeCounter();
b();
Variables a and b point at function objects, and each function points at the page where it was createdNAMESFUNCTIONSPAGES
The definition is put on the shelf. Nothing runs yet.
1/9

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.

Definition

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
Try itTwo private counters
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.)

Under the hood

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

ExerciseA private running total

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));
Extra practiceRun only once

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. let and const are 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. 1.

    What happens?

    if (true) {
      const z = 1;
    }
    console.log(z);
  2. 2.

    What does this print?

    const x = 'outer';
    function show() { console.log(x); }
    function run() {
      const x = 'inner';
      show();
    }
    run();
  3. 3.

    const a = makeCounter(); const b = makeCounter(); Then a(); a(); a();. What does b() return?

  4. 4.

    What does this print?

    let n = 1;
    const f = () => n * 10;
    n = 5;
    console.log(f());
  5. 5.

    Which best describes a closure?

  6. 6.

    Why is makeCounter’s count safe from the rest of the program?

References

  1. [1] MDN contributors. Closures (JavaScript Guide). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures (checked 2026-10-31)