Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~26 min· topic 6 of 9

Topic 3.6

Closures

In one line

A closure is a function together with the variables of the place where it was created; the function keeps using those variables even after the outer function has finished, which gives you counters, private state and function factories, and explains the classic var loop bug.

Think of it like this

A school backpack. When a child leaves home in the morning, they carry a backpack packed at home: lunch, a pencil case, a note from their parents. At school, far from home, they can still open the backpack and use what's inside, and even add or eat things. A closure is a function carrying a backpack of the variables from the place it was made. Each child packed at a different time gets their own backpack.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Closure
A function plus the variables from the place it was created, which it keeps using later.
Inner function
A function defined inside another function.
Outer function
The function that contains the inner function's definition.
Capture
When an inner function uses an outer variable, it captures it: it keeps a live link to it.
Function factory
A function that builds and returns new functions, each with its own captured variables.
Private state
Data that only certain functions can read or change, because no outside name reaches it.
Encapsulation
Hiding data and allowing changes only through a few chosen functions.
Binding
The link between a name and its storage. let in a for loop makes a new one each iteration.

Step by step

01Write a function that returns a function

makeCounter declares a local count, then returns an inner function that uses it.

counter.jswhole filejavascript
function makeCounter() {
  let count = 0;
  return function () {
    count = count + 1;
    return count;
  };
}

02Call the outer function once

const next = makeCounter(); runs makeCounter to the end. Normally count would vanish now. But the returned function uses it, so count is kept alive, attached to next.

03Call the closure several times

Each call reads and updates the same remembered count. The value survives between calls.

counter.jswhole filejavascript
const next = makeCounter();
next(); // 1
next(); // 2
next(); // 3
Call the closure several timesdiagram
Rendering diagram…

04Make a second counter

Calling the factory again creates a brand-new count. The two closures never share.

counter.jswhole filejavascript
const other = makeCounter();
other(); // 1, not 4

05Try to reach the private variable

From outside, count doesn't exist: typeof count is "undefined" and next.count is undefined. The only door in is the function you returned.

06Understand the var loop bug

With var, all loop callbacks share one i. With let, each iteration gets its own i. Same code, one keyword different.

loop.jswhole filejavascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 10); // 3 3 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 10); // 0 1 2
}

Try it yourself

  1. 1

    Add a reset

    In the first example, change makeCounter to return an object { next, reset }, where reset sets count back to 0. Predict: does resetting a affect b?

  2. 2

    Add a rule

    In the wallet example, make spend refuse amounts of 0 or less with the message "Amount must be positive". Can outside code skip your rule? Try.

  3. 3

    Fix the var loop without let

    In the third example, keep var k but wrap the setTimeout in (function (saved) { setTimeout(() => console.log("var k:", saved), 10); })(k);. Predict the new output.

Code & diagrams

A counter that remembers New tab

`a` and `b` each carry their own `count`. Nothing outside can reach it.

Console

(nothing was printed)

Expected console output

1 2 3
1
undefined undefined
10 15
Private state with rules New tab

The buttons are closures too: each arrow remembers `wallet`. Click Spend until the rule stops you.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

150
Not enough money
120
undefined
120
The loop + var bug, and the let fix ES2015+ New tab

All the `var` callbacks share one `i` (or `k`), which is 3 when they run. `let` gives every iteration its own variable.

Console

(nothing was printed)

Expected console output

[ 3, 3, 3 ]
[ 0, 1, 2 ]
var k: 3
var k: 3
var k: 3
let m: 0
let m: 1
let m: 2

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Create the counter inside the call

Write console.log(makeCounter()(), makeCounter()(), makeCounter()()).

terminal
$ Console
── what you'll see ──
1 1 1

Break #2

Loop handlers all point at the last item

Attach click handlers in for (var i = 0; i < buttons.length; i++) { buttons[i].onclick = () => show(i); }.

terminal
$ click any button
── what you'll see ──
Every button shows the same number: buttons.length

Myth vs fact

Myth

A closure copies the variable's value when it's created.

Fact

It keeps a live link to the variable itself. Later changes are visible, and the closure can change it too.

Myth

Closures are an advanced feature you create on purpose.

Fact

Every function that uses an outer variable is a closure. You use them with every callback you write.

Myth

Closures cause memory leaks.

Fact

They keep captured variables alive only while the closure itself is reachable. Leaks happen when a closure is kept forever by mistake, such as a listener that's never removed.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Engines don't keep the whole outer scope alive: V8 analyses which variables inner functions actually use and moves only those into a heap-allocated Context; the rest stay on the stack and disappear. But all closures created in the same scope share one Context, so a tiny closure can keep a big variable alive if a sibling closure captured it.

  • ▸

    The per-iteration let binding in for loops is specified in CreatePerIterationEnvironment: each iteration copies the current value into a fresh binding before the update expression runs.

  • ▸

    In DevTools, pause inside a function and look at the Scope pane: the Closure section lists exactly which outer variables that function captured. Heap snapshots show closures as (closure) entries with their context.

  • ▸

    Classes with #private fields (ES2022) now offer true privacy with methods shared on the prototype; closures give privacy per object but create new method functions for every instance. Both are valid; pick by how many instances you create.

Remember this

  1. 1

    Normally, when a function finishes, its local variables are thrown away. But if the function creates an inner function that uses those variables, and that inner function survives (it's returned, stored, or used as a callback), the variables can't be thrown away: the inner function still needs them. The inner function plus the variables it remembers is called a closure. You don't do anything special to make one: every JavaScript function remembers the scope it was created in (that's lexical scope, Topic 3.4).

  2. 2

    The remembered variables are live, not a snapshot. The closure holds a reference to the variable itself, so it reads the current value, and if it changes the variable, the change sticks for the next call. That's how a counter works: function makeCounter() { let count = 0; return () => ++count; }. Each call to the returned function bumps the same count.

  3. 3

    Each call to the outer function creates a fresh set of variables, so each returned function has its own backpack. const a = makeCounter(); const b = makeCounter(); gives two independent counters: a() moving to 3 doesn't touch b. This pattern of a function that builds and returns new functions is called a function factory.

  4. 4

    Closures give you private state. Code outside makeCounter has no way to reach count directly: no name refers to it from outside. The only way to change it is through the functions you returned, so you can enforce rules ("never go below zero", "only positive deposits"). This is called encapsulation, and it's how JavaScript had private data long before classes got #private fields (ES2022).

  5. 5

    The famous **loop + var bug**: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 10); } prints 3 3 3, not 0 1 2. There is only one var i for the whole function, and all three callbacks close over that same variable; by the time they run, the loop has finished and i is 3. With let, the spec creates a **new binding of i for every iteration**, so each callback closes over its own copy: 0 1 2. Before let existed, people fixed it with an IIFE that took i as a parameter (Topic 3.9).

  6. 6

    Closures are everywhere once you look: event handlers that remember which item they belong to, setTimeout callbacks, once, debounce and memoize helpers (Topic 3.7), React hooks and module patterns. The cost to keep in mind: everything a closure can reach stays in memory as long as the closure is alive, so a long-lived closure (like an event listener never removed) that captures a large object keeps that object alive too.

Explain it without notes

01

What is a closure? Explain with a counter example.

02

Why do two counters made by the same factory not share their count?

03

Explain why a for loop with var and setTimeout prints the same number three times, and two ways to fix it.

04

How do closures provide private state, and what is a downside?

Practice

01

Write makeGreeter(greeting) that returns a function taking a name, so makeGreeter("Hi")("Asha") returns "Hi, Asha".

02

Write makeCounter(start = 0) that returns { increment, decrement, value } where value() returns the current count.

03

Write once(fn) that returns a function which calls fn only the first time and returns the same result on every later call.

Trade-offs

  • ↔

    Closure-based privacy is simple and truly private, but each object gets its own copies of the functions; class with #private fields shares methods and is easier to inspect.

  • ↔

    Long-lived closures (listeners, caches) are convenient but keep everything they capture in memory; remove listeners and clear caches you no longer need.

Done when you can

  • I can define a closure in one sentence and point to one in code.

  • I can build a counter and a function factory with closures.

  • I know captured variables are live, not copies.

  • I can explain and fix the loop + var bug.

  • I can use a closure to keep state private behind a few functions.