Lesson 13 / 17
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.
Contents
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.
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.
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.
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.
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.
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.
WeakMapandWeakSetaccept only objects; they do not supportsize, 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.
WeakRefandFinalizationRegistryare 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.