Course outline

JavaScript / Basic: values and browser foundations / Lesson 4

Closures and private state

Follow the lifetime of lexical bindings and build independent stateful functions. Counters, loop callbacks, and immutable snapshots reveal what a closure retains and when it reads it.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain captured bindings and independent invocations.
  • Distinguish a live binding from a snapshot.
  • Manage ownership and lifetime of retained state.

A returned function retains access

A closure combines a function with access to its surrounding lexical environment. Returning a function does not discard the bindings it still needs. Each call to a factory creates a distinct invocation environment, so two counters can maintain independent totals. The retained state is not copied on each call to the returned function: a later invocation sees changes made to the same captured binding. This is useful for encapsulation, but it also means a callback can observe a newer value than the one present when it was registered. Predict which environment owns each binding.

javascript Independent environments from the same factory
function makeCounter() {
  let count = 0;
  return () => ++count;
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), b(), a());
Expected output
1 2 1 3

a retains one count binding and b retains another. Repeated a calls update its existing binding.

Choose live observation or a snapshot

For a confirmation message, decide whether it should describe the value at scheduling time or the latest value when shown. Capturing a mutable binding provides live access; storing a primitive in a new const binding captures that primitive value for the new environment. Object snapshots need an appropriate copy strategy because a const reference can still reach changing properties. This decision matters for delayed saves and analytics summaries: silently observing the latest draft may attach the wrong label to an earlier action. Make the intended observation time part of the functionโ€™s contract.

Loop bindings affect later callbacks

A let declaration in the head of a for loop provides per-iteration bindings for the relevant closure behavior. By contrast, var has function scope and callbacks can all observe its final value after the loop. Understand the binding lifetime rather than adding arbitrary timeouts or wrapper tricks. In the example, no timer is needed: the functions are stored and invoked after iteration, making the capture visible deterministically. Use the same reasoning for buttons created from a list; each handler needs the identifier for its intended item, not a shared index that has already advanced.

javascript Per-iteration state and a live outer binding
const handlers = [];
for (let i = 0; i < 3; i++) handlers.push(() => i);
console.log(handlers.map(fn => fn()).join(","));
let label = "draft";
const read = () => label;
label = "saved";
console.log(read());
Expected output
0,1,2
saved

The three functions retain distinct i bindings. read retains access to label, so it returns its later value.

Expose operations, not arbitrary mutation

A reservation factory can keep a count private and expose reserve and available operations that enforce a capacity invariant. Callers cannot reassign the lexical binding directly, but returning the mutable internal object would let them bypass the intended operations. Encapsulation therefore includes decisions about returned references. An accessor can return a primitive or a deliberate snapshot instead of the live collection. Closures are one tool for this design, alongside private class fields and modules. Select a representation that makes state transitions reviewable rather than treating hidden state as inherently correct.

A useful closure can outlive its UI

A listener, timer, or cache can keep a closure reachable, and that closure can retain a large document fragment or data array. Automatic garbage collection cannot reclaim objects that remain reachable through those paths. When a component is removed, release registrations and references it owns; do not rely on a particular moment of collection. A small explicit dispose operation makes the lifecycle testable. Avoid capturing an entire response when a stable identifier is sufficient. Later memory diagnostics build on the same question introduced here: which reachable owner still needs this state?

Common errors and fixes

Every callback uses the last item

Why it happens
Callbacks share one changing function-scoped binding.
How to fix it
Use per-iteration let bindings or pass a stable item explicitly.
Prevent it next time
Invoke stored handlers in a deterministic test.

A supposed snapshot changes later

Why it happens
The closure retains a live binding or shared object.
How to fix it
Capture the required primitive or copy the required fields.
Prevent it next time
Define whether observation is at creation or invocation.

A removed view retains large data

Why it happens
A long-lived registration still references its closure.
How to fix it
Remove owned registrations and clear unnecessary references.
Prevent it next time
Pair setup with an explicit disposal path.

Check your understanding

1. Predict two factories each returning a counter starting at ten.

Show worked solution

Each factory call has its own binding. If the returned function increments before returning, calls a(), a(), b() produce 11, 12, 11. The shared source text does not imply shared invocation state; a deliberate module-global binding would create a different design.

2. A delayed receipt should retain the submitted amount, not the edited amount. What should it capture?

Show worked solution

Create a new binding containing the validated numeric submitted amount and capture that value for the receipt. Do not read the live form or mutable draft later. If other nested receipt data is required, copy only the fields needed under an explicit snapshot policy.

Key takeaways

  • Closures retain access to bindings, not an automatic frozen history.
  • State privacy and state lifetime both need intentional ownership.

Official references