Closures

The Course
•
5 min read

A function can retain its surrounding variables

A closure is a function together with access to the variables where it was created. You met this when a row’s click handler remembered its index. Now use it deliberately: a factory function creates local state and returns functions that use it later.

Each factory call creates a separate environment. Two counters made by two calls do not share their count. Within one counter, however, all returned methods observe that instance’s same changing variable. The remembered value is not frozen at creation time.

The example uses object method shorthand, next() { ... }, to store a function under a property name. It is similar to next: function () { ... }. These methods use their surrounding variable and do not depend on this. We will use this approach for controllers rather than adding class syntax before it solves a problem.

Use a closure when a value needs to survive between calls but does not belong to the whole program. Keep the API small so callers cannot bypass its rules. Returning the raw mutable internal array would expose that state again; return a copy when callers need a snapshot.

A later factory uses a configuration record as its parameter: function createController({ search, render }). This destructures the supplied record immediately, giving the function local names search and render. It is equivalent to accepting options and reading options.search and options.render inside the body. The caller passes one record, not two positional arguments.

Predict and trace

Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.

Predict, then run JavaScript
function createCounter() {
  let count = 0;

  return {
    next() {
      count += 1;
      return count;
    }
  };
}

const first = createCounter();
const second = createCounter();

console.log(first.next());
console.log(first.next());
console.log(second.next());
Check your prediction
1
2
1

Each call owns count. Calling first.next does not read second’s count or recreate first’s environment.

A factory call owns one lifetime of state

A closure retains access to the bindings created by its surrounding call. Calling the factory again creates a different set of bindings. Putting state outside the factory changes the contract: all returned objects would then share it.

Return snapshots when callers should inspect data without editing the internal container. A snapshot of an array is shallow; if entries are objects, they may still be shared. Choose a small, testable ownership promise rather than assuming the word closure makes all state private in every sense.

A factory call owns one lifetime of state: trace JavaScript
function createList() {
  const items = [];

  return {
    add(value) {
      items.push(value);
    },
    snapshot() {
      return [...items];
    }
  };
}

const first = createList();
const second = createList();

first.add("a");
console.log(first.snapshot().length, second.snapshot().length);
Check your prediction
1 0

Two factory invocations own separate arrays. The methods from one invocation share its array, which is exactly the intended collaboration.

A factory call owns one lifetime of state: contrast JavaScript
function createList() {
  const items = ["a"];

  return {
    snapshot() {
      return [...items];
    }
  };
}

const list = createList();
const snapshot = list.snapshot();

snapshot.push("outside");
console.log(snapshot.length, list.snapshot().length);
Check your prediction
2 1

Editing the copied array does not edit the internal array. Returning items directly would make this same test expose a leak through the public API.

Try the rule on new inputs

Move the array outside the factory and confirm the independent-instance test changes. Then try nested object entries and explain the limit of the shallow snapshot promise.

Build it yourself

Exercise

Closures: practise

Write createIdSource(prefix), returning a next method that produces read-1, read-2, and an independent other-1 for a second instance.

Your working space JavaScript
// Each factory call needs its own counter.
Hint 1: the idea

Keep the counter inside the factory, outside its returned method.

Hint 2: a first step

Use prefix and the updated counter to build the returned string.

Show one solution
function createIdSource(prefix) {
  let count = 0;

  return {
    next() {
      count += 1;
      return `${prefix}-${count}`;
    }
  };
}

const a = createIdSource("read");
const b = createIdSource("other");

console.log(a.next());
console.log(a.next());
console.log(b.next());
read-1
read-2
other-1

Create a third source with the same prefix. Its values repeat: this is a local ID source, not a globally unique ID generator. State that limitation.

Test a changed requirement

Create a third source with the same prefix. Its values repeat: this is a local ID source, not a globally unique ID generator. State that limitation.

Explain before moving on

What must move if all instances accidentally share a count? Why is an instance different from one call to its method?

Was this article helpful?

Your response is saved on this device.