JS Sketchbooksee JavaScript think ✏️
← back to phase 3

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.

watch it happen
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 call stackglobal(frame gone)
console(nothing logged yet)
under the hood

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.

your turn

⌨️ 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 makeGreeter with 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 call greetSam() twice — same greeting both times, from a factory call that finished long ago.

when you press RUN, the console must show exactly:

Hello, Sam!
Hello, Sam!

✏️ 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.

teach it back

🗣️ 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.

a few sentences, minimum — you’ve got this
to remember
A closure = a function wearing a backpack of the variables from where it was BORN. They stay alive after the outer call dies.
The backpack holds the living variable, not a copy — that’s why count keeps advancing between calls.
Every factory call sews a fresh backpack: two counters never share state. Persistent AND private.
The debt from 3.2 is paid: slots die with the call — UNLESS a returned inner function carries them away.