3.7 — Closures
Time to pay a debt. Back in lesson 3.2 you learned that a function’s slots are born at the call and destroyed when it ends — and I promised you a spectacular exception. This is it. Today you meet a function that walks away from its birthplace still carrying its variables — alive, private, and remembering.
Why would you want that? Think of a counter: something must remember the count between clicks, but nothing outside should be able to tamper with it. Scope’s one-way glass (3.5) gives the privacy; today’s trick gives the memory. Together they’re called a closure — the concept JavaScript interviewers love most, about to become a picture you can’t forget.
function makeCounter() {
let count = 0;
return function () {
count = count + 1;
console.log(count);
};
}
const tick = makeCounter();
tick();
tick();The engine stores makeCounter. Notice what its body does: it creates a variable count, and then RETURNS A FUNCTION (allowed since 3.4 — functions are values!). The inner function uses count, a variable from its birthplace.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
The precise definition: a closure is a function together with its lexical environment. That environment is the outer variables in scope where the function was defined — not where it is called.
When the engine sees an inner function referencing an outer variable, it keeps that variable alive. It lives on in a longer-lived memory area (the heap — Phase 4 draws it properly) instead of dying with the stack frame. The inner function “closes over” the variable — hence the name.
Two facts worth engraving: closures capture the variable, not a snapshot of its value — that’s why count keeps advancing. And each call to the outer function creates a new environment: makeCounter() twice gives two independent counters with two private counts.
This combination — persistent + private — is how JavaScript did “private state” for decades before classes got #private fields.
💼 On the job — this is why a Playwright helper like makeLogin(page) can hand back a function that still knows its page, wherever the call happens. Backpacks everywhere.
⌨️ build a machine that remembers
A factory that builds personalized greeting machines — each one remembering its own name forever, long after the factory call is gone.
requirements:
- A function named
makeGreeterwith one input slot:name. It RETURNS a function. - The returned function, when called, prints
Hello, <name>!— using the name it remembers from its backpack. - Prove the memory:
const greetSam = makeGreeter("Sam");then callgreetSam()twice — same greeting both times, from a factory call that finished long ago.
when you press RUN, the console must show exactly:
✏️ Quick check 1
Type what the SECOND tick() prints:
✏️ Quick check 2
Two factories, two backpacks — type what b() prints:
const a = makeCounter(); const b = makeCounter(); a(); a(); b();
✏️ Quick check 3
A function bundled with the outer variables from its birthplace — type the name of this concept.
🗣️ Now teach it back
Explain closures to a friend with the backpack picture — including why lesson 3.2’s "slots die with the call" rule has this exception, and why two counters from the same factory never share a count.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.