Skip to content
academia.sh

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.
  • 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close