---
title: 'Weak Collections'
source: 'https://academia.sh/en/courses/javascript-object-model/weak-collections'
course: 'Objects and Functions in JavaScript'
language: en
updated: '2026-08-17T18:09:50+00:00'
license: 'CC BY-SA 4.0'
---

# Weak Collections

Mapping and set types that do not prevent their keys from being collected, the pattern for attaching data to an object from outside, why they cannot be iterated, and weak references.

The previous lesson ended with `Map` and left a problem behind: as long as a mapping
holds an object as a key, that object stays reachable. Tables set up to attach extra
information to an object are therefore a source of leaks — every entry not removed from
the table keeps a no-longer-used object alive.

`WeakMap` and `WeakSet` solve this problem. The reference they hold to the objects they
keep is **weak**: an object unreachable from anywhere else does not stay alive just
because it is in a weak collection. In exchange, a few capabilities are given back; the
lesson examines this trade-off.

## Weak Mapping

`WeakMap` is a restricted form of the `Map` structure: its keys can only be objects,
element count cannot be read, and its content cannot be iterated.

```js
const record = { sensor: "S-01", value: 21.4 };
const otherRecord = { sensor: "S-02", value: 19.8 };

const weakLedger = new WeakMap();
weakLedger.set(record, { sourceFile: "measurement-2.csv", line: 17 });
weakLedger.set(otherRecord, { sourceFile: "measurement-3.csv", line: 4 });

console.log(weakLedger.has(record));
console.log(weakLedger.get(record).line);
console.log(weakLedger.get({ sensor: "S-01", value: 21.4 }));

try {
  weakLedger.set("S-01", { line: 1 });
} catch (error) {
  console.log(`primitive key: ${error.constructor.name}`);
}

console.log(weakLedger.size);
console.log(typeof weakLedger.keys);
console.log(typeof weakLedger[Symbol.iterator]);

try {
  for (const _ of weakLedger) console.log(_);
} catch (error) {
  console.log(`iteration: ${error.constructor.name}`);
}

console.log(weakLedger.delete(record));
console.log(weakLedger.has(record));
console.log(JSON.stringify(weakLedger));
```

```
true
17
undefined
primitive key: TypeError
undefined
undefined
undefined
iteration: TypeError
true
false
{}
```

The key-identity rule is the same as `Map`'s: another object with the same content is a
different key. When a primitive value is given as a key, an error is thrown, because
"collectibility" is not defined for primitive values — strings are shared values, not
objects whose identity is tracked.

The three missing capabilities — `size`, `keys`, and iteration — are not a shortcoming,
they are a direct result of the design. Being able to list the content would mean the
program could observe when the garbage collector runs; the same program run twice could
give a different result. Non-iterability is the price paid to preserve determinism.

## Attaching Data to an Object from Outside

The most common use of a weak mapping is keeping information belonging to an object
**outside** the object. Private fields in class syntax are the language-level
counterpart of this need; a weak mapping produces the same result with library code.

```js
const privateData = new WeakMap();

class MeasurementRecord {
  constructor(sensor, rawValue, offset) {
    this.sensor = sensor;
    privateData.set(this, { rawValue, offset, readCount: 0 });
  }

  get reading() {
    const data = privateData.get(this);
    data.readCount += 1;
    return data.rawValue + data.offset;
  }

  status() {
    return `reads=${privateData.get(this).readCount}`;
  }

  static hasData(obj) {
    return privateData.has(obj);
  }
}

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

console.log(record.reading);
console.log(record.reading);
console.log(record.status());
console.log(Object.keys(record).join(","));
console.log(JSON.stringify(record));
console.log(MeasurementRecord.hasData(record));
console.log(MeasurementRecord.hasData({ sensor: "S-02" }));

const detachedStatus = record.status;
try {
  detachedStatus();
} catch (error) {
  console.log(`detached call: ${error.constructor.name}`);
}
```

```
21
21
reads=2
sensor
{"sensor":"S-01"}
true
false
detached call: TypeError
```

The raw value, offset, and read counter are not properties of the record: they are not
listed, not serialized, and cannot be read by code with no access to the `privateData`
reference. The result is the same as private fields in the Class Syntax lesson and the
factory setup in the Closures lesson.

The difference among the three paths is where they are stored. A private field is stored
inside the object, a closure in scope, a weak mapping outside the object but keyed to it.
What sets the third apart is that it can attach data **without changing the object**:
even an object whose class you did not write, even a frozen one, can have information
bound to it this way.

The last line repeats a shared limit: because the data is found through `this`, the
method still throws an error when detached from its object. A weak mapping does not
change binding rules.

## Weak Set

`WeakSet` applies the same principle to a set: it is used for marking objects, its
content cannot be iterated.

```js
const processed = new WeakSet();

function process(record) {
  if (processed.has(record)) return "already processed";
  processed.add(record);
  return `processed: ${record.sensor}`;
}

const first = { sensor: "S-01", value: 21.4 };
const second = { sensor: "S-02", value: 19.8 };

console.log(process(first));
console.log(process(first));
console.log(process(second));
console.log(processed.has(first));
console.log(processed.has({ sensor: "S-01", value: 21.4 }));

try {
  processed.add("S-03");
} catch (error) {
  console.log(`primitive value: ${error.constructor.name}`);
}

console.log(processed.size);
console.log(typeof processed.values);
```

```
processed: S-01
already processed
processed: S-02
true
false
primitive value: TypeError
undefined
undefined
```

The usage pattern is singular: "has this object been processed before?" This was the
reason `WeakSet` was chosen for visit-marking in the deep-freeze function of the
Immutability Techniques lesson — the marking is temporary and should not extend the
lifetime of the objects being walked.

Cycle detection based on object identity, preventing a repeated operation, and testing
whether an object was produced by a specific constructor are different names for the same
pattern.

## The Cost of a Strong Reference

The memory difference between the two collection types can be seen without observing
garbage collection at all: a strong mapping can count and list its content, a weak
mapping cannot.

```js
const strongCache = new Map();
const weakCache = new WeakMap();

function processMeasurement(record, cache) {
  if (cache.has(record)) return cache.get(record);
  const result = `${record.sensor}:${record.value.toFixed(1)}`;
  cache.set(record, result);
  return result;
}

for (let i = 0; i < 1000; i += 1) {
  const temp = { sensor: `S-${i}`, value: i / 10 };
  processMeasurement(temp, strongCache);
  processMeasurement(temp, weakCache);
}

console.log(strongCache.size);
console.log([...strongCache.values()][0]);
console.log([...strongCache.keys()][999].sensor);
console.log(typeof weakCache.size);

const persistent = { sensor: "S-K", value: 30 };
console.log(processMeasurement(persistent, weakCache));
console.log(weakCache.has(persistent));
```

```
1000
S-0:0.0
S-999
undefined
S-K:30.0
true
```

Once the loop ends, the thousand temporary records inside it are, from the program's
point of view, unusable — the `temp` variable is rebound on every iteration, and none of
the earlier ones is reachable from anywhere after the last iteration. Despite this, because
the strong mapping holds all of them as keys, both the thousand objects and the thousand
result strings stay reachable; the `keys` call shows this directly.

In the weak mapping, the same entries are meaningful only as long as the key object stays
reachable from somewhere else. The last two lines show this: because the `persistent`
object is held in a variable, its entry is found. When entries for keys that become
unreachable get cleaned up is not scheduled in the specification and cannot be observed
by the program; no output in this lesson depends on the moment of collection.

How reachability-based garbage collection works is the subject of the Asynchronous
JavaScript and the Runtime course.

## Weak References

Alongside weak collections, two more constructs offer the same principle for a single
object. `WeakRef` holds a weak reference to an object; a `deref` call gives the object if
it is still reachable, `undefined` if not. `FinalizationRegistry` registers a callback to
run after an object is collected.

```js
const record = { sensor: "S-01", value: 21.4 };
const weakRef = new WeakRef(record);

console.log(weakRef.deref() === record);
console.log(weakRef.deref().sensor);

const log = [];
const registry = new FinalizationRegistry((label) => {
  log.push(label);
});
registry.register(record, "S-01 collected");

console.log(typeof registry.register);
console.log(typeof registry.unregister);
console.log(log.length);
console.log(weakRef.deref() !== undefined);
```

```
true
S-01
function
function
0
true
```

The output's last two lines hold as long as the `record` variable stays alive: the log
is empty and the reference resolves. **When** the callback runs — even whether it runs at
all before the program ends — is not guaranteed by the specification. For this reason,
these two constructs are not made the foundation of program logic; their use cases are
diagnostics, measurement, and optional caches.

For the same reason, no block in this lesson shows an output that depends on the moment
of collection: such an output would vary with the runtime and the memory state at that
moment.

## Summary

- Weak collections hold a weak reference to the objects they keep; being present there
  alone does not keep an object alive.
- `WeakMap` and `WeakSet` accept only objects; they do not support `size`, iteration, or
  serialization — this restriction preserves determinism.
- A weak mapping attaches data to an object without changing it at all; it is the third
  option alongside the hiding set up with private fields and closures.
- A weak set is used to mark objects: cycle detection, preventing a repeated operation,
  membership testing.
- A strong mapping keeps its keys reachable; in caches keyed to objects, this is the
  source of unbounded growth.
- `WeakRef` and `FinalizationRegistry` are not made the foundation of program logic
  because their timing is not guaranteed by the specification.

## Next Step

In this lesson, a `for...of` attempt on `WeakMap` threw an error, while the same loop
works on `Map`, `Set`, an array, and a string. The difference is not a list of types, it
is a single contract: whether an object is iterable depends on it offering a specific
method. The next lesson opens that contract — the iteration protocol — and gives the
measurement series its own traversal behavior.
