Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 6

Closures

This chapter assumes you are comfortable with function declarations vs. expressions, lexical scope with `let`/`const`, and the difference between a value and a reference. We go beneath the surface: what a closure actually is at runtime, where it silently costs you memory, and when to prefer it over
Level: AdvancedLength: In depth4 min read

Read at another depth

Closures

This chapter assumes you are comfortable with function declarations vs. expressions, lexical scope with let/const, and the difference between a value and a reference. We go beneath the surface: what a closure actually is at runtime, where it silently costs you memory, and when to prefer it over adjacent tools like classes or partial application libraries.

The Working Model

A closure is a function paired with the scope (the set of variable bindings) it was created in. Whenever a function is defined, it captures a reference to its enclosing environment record — the runtime structure holding that scope's bindings. The function keeps that reference alive for as long as the function itself is reachable, regardless of whether the enclosing function has returned.

The key consequence: inner functions outlive the activation of the function that created them, and the variables they reference are not copied — they are shared, live bindings.

Code example in javascript
function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2

makeCounter finishes executing after the first line of the last two statements, yet count survives. next holds a reference to makeCounter's environment. Each call to makeCounter produces a distinct environment — a second counter is independent.

Code example in javascript
const a = makeCounter();
const b = makeCounter();
a(); a(); // 3 from a's closure
b();      // 1 — separate environment

What the Engine Actually Does

Modern engines allocate environment records on the heap when a scope might be captured. In V8, variables that are closed over are moved from the stack frame into a heap-allocated Context object. The function's internal [[Environment]] slot points at it; when the function body reads count, a scope-chain lookup walks [[Environment]] first, then outward.

This is why closures are not snapshots. If the captured binding is later reassigned, the closure sees the new value:

Code example in javascript
let x = 1;
function read() { return x; }
x = 2;
console.log(read()); // 2 — live binding, not a copy

A useful mental refinement: the closure captures the binding, not the value — but if the binding holds a reference to an object, the closure shares that object.

Code example in javascript
function attach(state) {
  return { get: () => state.value, set: (v) => { state.value = v; } };
}
const s = { value: 10 };
const h = attach(s);
h.set(99);
console.log(s.value); // 99 — same object, mutated through the closure

Canonical Uses

  1. Private state. Before private class fields (#count), closures were the standard encapsulation tool. They remain the right choice when you want per-instance privacy without a class.
  2. Factories and partial application. A function returning a function with some arguments pre-bound:
Code example in javascript
const logWith = (level) => (msg) => console.log(`[${level}] ${msg}`);
const warn = logWith('WARN');
warn('disk nearly full'); // [WARN] disk nearly full
  1. Callbacks and event handlers. Every setTimeout, promise .then, or event listener that references enclosing variables is a closure. You have been writing them constantly.
  1. Memoization and once-style wrappers:
Code example in javascript
function once(fn) {
  let done = false;
  let result;
  return (...args) => {
    if (!done) { result = fn(...args); done = true; }
    return result;
  };
}

Gotchas and Failure Modes

The loop-capture bug

Code example in javascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// prints 3, 3, 3 — `var i` is one function-scoped binding

With let, each iteration creates a fresh per-iteration binding, so the closures capture different is and print 0, 1, 2. This is not a lint rule; it is a semantic difference in the spec. If you must support legacy var code, wrap the body in an IIFE to force a new scope.

Stale captures in React-style render closures

Closures freeze bindings at definition time in the sense that a callback created in one invocation captures that invocation's bindings. A callback registered once but reading state that later changed elsewhere will read its captured binding, which may be stale relative to external state. The fix is to re-create the callback when the values it reads change, or pass values as arguments rather than reading them from the enclosing scope.

Over-capturing

Capturing a large object "because it's convenient" pins it in memory for the lifetime of the closure. If a listener registered on a long-lived global closes over a component's entire state tree, that tree cannot be collected until the listener is removed.

Closure vs. Class vs. Module: Decision Criteria

NeedClosureClassModule
One private value + behaviorBest fitFine (`#field`)Overkill
Many instances sharing methodsWasteful (methods rebuilt per call)Best (methods on prototype)—
Many private fieldsAwkward`#fields` read naturally—
One-off encapsulated setupIdiomaticHeavyFine
State shared across an app——Module-level binding

The prototype-method argument matters at scale: with a closure-based factory, each instance allocates new function objects; with a class, methods live once on Greeter.prototype and instances only carry their own data.

Code example in javascript
class Greeter {
  greet(name) { return `Hello, ${name}!`; }
}
const g = new Greeter();
console.log(g.greet('Ada')); // Hello, Ada!
console.log(Object.getPrototypeOf(g) === Greeter.prototype); // true

Retention and Lifetime

To reason about leaks, ask: what holds a reference to this closure, and what does the closure's environment reference? A closure is collectable only when nothing references it; its environment dies with it. The dangerous patterns are all the same shape — a long-lived holder (global cache, event target, promise chain, module-level array) referencing closures that reference big objects. Mitigations: explicit teardown (removeEventListener, abort signals), weak holders (WeakMap, WeakRef) for cache-like semantics, and copying out only the values a callback actually needs.

Exercises

Exercise

Why does next() keep incrementing count after makeCounter has returned?

Show answer

The returned function carries a reference to makeCounter's heap-allocated environment record. count lives in that record, so it persists and is shared across all calls to next.

Exercise

A button's click handler was registered once at startup and reads config.endpoint from its enclosing scope. The config object is later replaced with a new object. Which endpoint does the handler use, and why?

Show answer

The old one. The handler captured the binding as it existed in the registration scope; reassigning config in that same scope would be visible, but replacing the value in a different scope/module (or mutating a different object) does not affect the handler's captured environment. Re-create the handler or read config indirectly through a stable reference.

Exercise

Your service's memory grows steadily. Profiling shows many retained Context objects rooted at listener registrations on a singleton emitter. Give two remedies.

Show answer

(1) Deregister listeners during teardown (e.g., on shutdown or AbortSignal), and (2) narrow the capture: pass only the values the listener needs rather than closing over a large object, or store shared state in a WeakMap keyed by the emitter.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Closures

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that