---
title: Closures
source: 'https://academia.sh/en/courses/javascript-object-model/closures'
course: 'Objects and Functions in JavaScript'
language: en
updated: '2026-08-23T07:00:59+00:00'
license: 'CC BY-SA 4.0'
---

# Closures

The lifetime and sharing of closed-over variables, encapsulation through closures, comparing three constructions of the same kind, the loop-variable trap, and the memoization pattern.

In the previous lesson's last example, a counter kept its state not in an object
property but in a local variable of the function that produced it. The function
returned, but the variable kept living and was visible only to the functions returned
from it.

A **closure** is a function's ability to keep reaching the variables of the scope it
was defined in, even after that scope has ended. The Programming Fundamentals course's
Scope and Lifetime lesson introduced the concept; this lesson places it inside the
object model. The result that follows is decisive for the course's axis: a closure
offers a second way of construction that hides state without ever touching the
prototype chain.

## Encapsulation With a Closure

This time, the measurement record is produced with a factory function. The raw value
and calibration offset stay in the function's parameters; they are never written onto
the returned object at all.

```js
function createMeasurementRecord(sensor, rawValue, offset) {
  let readCount = 0;

  function readValue() {
    readCount += 1;
    return rawValue + offset;
  }

  return {
    sensor,
    read: readValue,
    format() {
      return `${sensor}: ${readValue().toFixed(2)}`;
    },
    check() {
      return `reads=${readCount}`;
    },
  };
}

const record = createMeasurementRecord("S-01", 21.4, -0.4);

console.log(record.read().toFixed(2));
console.log(record.format());
console.log(record.check());
console.log(JSON.stringify(record));
console.log(Object.keys(record).join(","));
console.log(record.rawValue);
console.log(record.readCount);
```

```
21.00
S-01: 21.00
reads=2
{"sensor":"S-01"}
sensor,read,format,check
undefined
undefined
```

The raw value, the offset, and the read counter appear in no listing operation; they
do not enter serialization; they cannot be read or written from outside. This is the
same result as class syntax's private fields, but the mechanism is entirely
different: privacy comes not from property descriptors but from scope. A variable is
accessible only to the functions that can see it, so the scope boundary itself
provides encapsulation.

The check counter reading `2` shows that the `format` call used the same inner
function too: all three functions share the same scope — and therefore the same
variables.

## Three Constructions of the Same Kind

The measurement record used throughout the course can now be built three separate
ways. Placing all three side by side makes the trade-off visible in a single output.

```js
function plainRecord(sensor, value) {
  return {
    sensor,
    value,
    format() {
      return `${this.sensor}: ${this.value}`;
    },
  };
}

function closureRecord(sensor, value) {
  return {
    format() {
      return `${sensor}: ${value}`;
    },
  };
}

class ClassRecord {
  constructor(sensor, value) {
    this.sensor = sensor;
    this.value = value;
  }
  format() {
    return `${this.sensor}: ${this.value}`;
  }
}

const plainA = plainRecord("S-01", 21.4);
const plainB = plainRecord("S-02", 19.8);
const closureA = closureRecord("S-01", 21.4);
const closureB = closureRecord("S-02", 19.8);
const classA = new ClassRecord("S-01", 21.4);
const classB = new ClassRecord("S-02", 19.8);

console.log([plainA.format(), closureA.format(), classA.format()].join(" | "));

console.log(`plain  : shared=${plainA.format === plainB.format}, own=${Object.keys(plainA).join("+")}, json=${JSON.stringify(plainA)}`);
console.log(`closure: shared=${closureA.format === closureB.format}, own=${Object.keys(closureA).join("+")}, json=${JSON.stringify(closureA)}`);
console.log(`class  : shared=${classA.format === classB.format}, own=${Object.keys(classA).join("+")}, json=${JSON.stringify(classA)}`);

console.log(Object.getPrototypeOf(plainA) === Object.prototype);
console.log(Object.getPrototypeOf(closureA) === Object.prototype);
console.log(Object.getPrototypeOf(classA) === ClassRecord.prototype);

const brokenClass = classA.format;
const brokenClosure = closureA.format;
try {
  brokenClass();
} catch (error) {
  console.log(`class broken: ${error.constructor.name}`);
}
console.log(`closure broken: ${brokenClosure()}`);
```

```
S-01: 21.4 | S-01: 21.4 | S-01: 21.4
plain  : shared=false, own=sensor+value+format, json={"sensor":"S-01","value":21.4}
closure: shared=false, own=format, json={}
class  : shared=true, own=sensor+value, json={"sensor":"S-01","value":21.4}
true
true
true
class broken: TypeError
closure broken: S-01: 21.4
```

The first line confirms all three constructions produce **the same behavior**. The
remaining lines list the differences:

- **Sharing.** Only in the class construction is the function a single one; in the
  other two, every object carries its own function. For a thousand records, the class
  produces one function, the others a thousand.
- **Visibility.** In the closure construction, since the data is never a property at
  all, serialization gives an empty object. If the data needs to leave, an explicit
  method has to be written.
- **Chain.** Because factory functions return an ordinary object literal, their
  prototype is `Object.prototype`; none of the paths for type querying, adding shared
  behavior, or deriving a subtype works.
- **Binding.** In the closure construction, since `this` is never used, the method
  keeps working even when detached from its object; a class method throws an error in
  the same situation.

This is where the selection criterion comes from. If many objects, type queries,
subtypes, and shared behavior are needed, a class; if a small number of objects, strict
privacy, and context-independent calls are needed, a closure. The two are not
interchangeable options but two constructions with different cost profiles.

## The Lifetime of a Closed-Over Variable

A closure closes over not a **value** but a **variable**. Functions sharing the same
scope see the same variable; closures produced from different calls have separate
scopes.

```js
function createMeasurementCounter(start) {
  let count = start;
  return {
    increment: () => (count += 1),
    decrement: () => (count -= 1),
    read: () => count,
  };
}

const first = createMeasurementCounter(0);
const second = createMeasurementCounter(100);

first.increment();
first.increment();
second.decrement();

console.log(first.read());
console.log(second.read());

function createBuffer() {
  const values = [];
  return {
    add(value) {
      values.push(value);
      return values.length;
    },
    average() {
      if (values.length === 0) return 0;
      return values.reduce((total, v) => total + v, 0) / values.length;
    },
    size: () => values.length,
  };
}

const buffer = createBuffer();
buffer.add(21.4);
buffer.add(19.8);
buffer.add(25.1);

console.log(buffer.size());
console.log(buffer.average().toFixed(3));
console.log(Object.keys(buffer).join(","));
```

```
2
99
3
22.100
add,average,size
```

The two counters are independent of each other; every `createMeasurementCounter` call
produces a new scope. In contrast, the three functions returned from the same call
share a single `count` variable — when one changes it, the other sees the change.

The lifetime rule follows from this: **a closed-over variable lives as long as the
last function that can see it remains reachable.** After the `createBuffer` function
returns, there is no outside reference to the `values` array at all, but because the
three functions can see it, the array stays in memory. This is where a closure's
memory cost comes from: storing a small function that closes over a large data
structure keeps that entire data structure alive. Memory lifecycle and
reachability-based collection are the subject of the Asynchronous JavaScript and the
Runtime course.

## The Loop Variable Trap

The best-known consequence of the rule that a closure closes over a variable shows up
in functions produced inside a loop.

```js
const varFns = [];
for (var counter = 0; counter < 3; counter += 1) {
  varFns.push(() => `sensor-${counter}`);
}

const letFns = [];
for (let counter = 0; counter < 3; counter += 1) {
  letFns.push(() => `sensor-${counter}`);
}

console.log(varFns.map((f) => f()).join(","));
console.log(letFns.map((f) => f()).join(","));

const manualFns = [];
for (var i = 0; i < 3; i += 1) {
  manualFns.push(
    (function (fixed) {
      return () => `sensor-${fixed}`;
    })(i),
  );
}
console.log(manualFns.map((f) => f()).join(","));
```

```
sensor-3,sensor-3,sensor-3
sensor-0,sensor-1,sensor-2
sensor-0,sensor-1,sensor-2
```

The counter declared with `var` is function-scoped: there is a **single variable**
across the whole loop, and all three closures close over it. Because the variable's
value is `3` when the loop ends, all three functions give the same result. `let`
produces a new binding on every iteration, so each closure closes over its own
variable.

The third block shows the fix used before block-scoped declarations entered the
language: opening a new scope with an immediately invoked function on every iteration
and fixing the counter as a parameter. This pattern is the next lesson's subject.

## A State-Carrying Wrapper

A closure's most productive use is adding state to a function without modifying it.
**Memoization**, introduced in the Programming Fundamentals course's Recursion lesson,
is exactly this: computed results are stored in a dictionary held in the closure.

```js
function memoize(fn) {
  const cache = Object.create(null);
  let realCalls = 0;

  function wrapped(key) {
    if (key in cache) return cache[key];
    realCalls += 1;
    const result = fn(key);
    cache[key] = result;
    return result;
  }

  wrapped.realCallCount = () => realCalls;
  return wrapped;
}

const openTable = { "S-01": -0.4, "S-02": 0.15, "S-03": 0 };
const safeTable = Object.assign(Object.create(null), openTable);

const openRead = memoize((sensor) => openTable[sensor] ?? 0);
const safeRead = memoize((sensor) => safeTable[sensor] ?? 0);

console.log(openRead("S-01"));
console.log(openRead("S-01"));
console.log(openRead("S-02"));
console.log(openRead("S-01"));
console.log(openRead.realCallCount());

console.log(typeof openRead("toString"));
console.log(typeof safeRead("toString"));
console.log(safeRead("toString"));
console.log(safeRead.realCallCount());
console.log(typeof openRead.cache);
```

```
-0.4
-0.4
0.15
-0.4
2
function
number
0
1
undefined
```

Only two of the four calls actually run the underlying function; the rest come back
from the cache. The counter is attached to the function as a property, but the counter
itself still sits in the closure — the last line confirms the `cache` dictionary is
not visible from outside.

The sixth and seventh lines show the cost of the first lesson's warning. In a
calibration table built with a plain object literal, asking for the `"toString"` key
finds the function coming from the chain, and the `??` operator does not treat it as
empty. In a table built with `Object.create(null)` there is no chain, so the correct
result comes back. The same precaution is required in any mapping whose keys come from
outside the program; the type the language sets aside for this job, `Map`, is covered
in the Advanced Collections topic.

## Summary

- A closure is a function's ability to keep reaching the variables of the scope it was
  defined in, after that scope has ended.
- The privacy a closure establishes comes not from property descriptors but from the
  scope boundary; because the data is never a property, it is not listed and not
  serialized.
- A closure construction is context-independent but shares no behavior and sets up no
  prototype chain; a class construction is the opposite.
- A closure closes over a variable, not a value; functions produced from the same
  scope share the same variable, separate calls produce independent scopes.
- A closed-over variable lives as long as the last function that can see it is
  reachable; this is where memory cost comes from.
- A loop counter declared with `var` is a single variable; `let` produces a new
  binding on every iteration.

## Next Step

In the loop trap's old fix, a function was called the instant it was defined, and it
was used only to open a new scope. This pattern has a name, and it was also the only
way to draw library boundaries before block-scoped declarations. The next lesson
covers immediately invoked functions, the scope-isolation job they do, and how far
that job has been taken over by the language's later abilities.
