JavaScript for impatient programmers (ES2022 edition)
Please support this book: buy it or donate
(Ad, please don’t block.)

34 WeakMaps (WeakMap) (advanced)



WeakMaps are similar to Maps, with the following differences:

The next two sections examine in more detail what that means.

34.1 WeakMaps are black boxes

It is impossible to inspect what’s inside a WeakMap:

These restrictions enable a security property. Quoting Mark Miller:

The mapping from weakmap/key pair value can only be observed or affected by someone who has both the weakmap and the key. With clear(), someone with only the WeakMap would’ve been able to affect the WeakMap-and-key-to-value mapping.

34.2 The keys of a WeakMap are weakly held

The keys of a WeakMap are said to be weakly held: Normally if one object refers to another one, then the latter object can’t be garbage-collected as long as the former exists. With a WeakMap, that is different: If an object is a key and not referred to elsewhere, it can be garbage-collected while the WeakMap still exists. That also leads to the corresponding entry being removed (but there is no way to observe that).

34.2.1 All WeakMap keys must be objects

All WeakMap keys must be objects. You get an error if you use a primitive value:

> const wm = new WeakMap();
> wm.set(123, 'test')
TypeError: Invalid value used as weak map key

With primitive values as keys, WeakMaps wouldn’t be black boxes anymore. But given that primitive values are never garbage-collected, you don’t profit from weakly held keys anyway, and can just as well use a normal Map.

34.2.2 Use case: attaching values to objects

This is the main use case for WeakMaps: you can use them to externally attach values to objects – for example:

const wm = new WeakMap();
{
  const obj = {};
  wm.set(obj, 'attachedValue'); // (A)
}
// (B)

In line A, we attach a value to obj. In line B, obj can already be garbage-collected, even though wm still exists. This technique of attaching a value to an object is equivalent to a property of that object being stored externally. If wm were a property, the previous code would look as follows:

{
  const obj = {};
  obj.wm = 'attachedValue';
}

34.3 Examples

34.3.1 Caching computed results via WeakMaps

With WeakMaps, you can associate previously computed results with objects without having to worry about memory management. The following function countOwnKeys() is an example: it caches previous results in the WeakMap cache.

const cache = new WeakMap();
function countOwnKeys(obj) {
  if (cache.has(obj)) {
    return [cache.get(obj), 'cached'];
  } else {
    const count = Object.keys(obj).length;
    cache.set(obj, count);
    return [count, 'computed'];
  }
}

If we use this function with an object obj, you can see that the result is only computed for the first invocation, while a cached value is used for the second invocation:

> const obj = { foo: 1, bar: 2};
> countOwnKeys(obj)
[2, 'computed']
> countOwnKeys(obj)
[2, 'cached']

34.3.2 Keeping private data in WeakMaps

In the following code, the WeakMaps _counter and _action are used to store the values of virtual properties of instances of Countdown:

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

// The two pseudo-properties are truly private:
assert.deepEqual(
  Object.keys(new Countdown()),
  []);

This is how Countdown is used:

let invoked = false;

const cd = new Countdown(3, () => invoked = true);

cd.dec(); assert.equal(invoked, false);
cd.dec(); assert.equal(invoked, false);
cd.dec(); assert.equal(invoked, true);

  Exercise: WeakMaps for private data

exercises/weakmaps/weakmaps_private_data_test.mjs

34.4 WeakMap API

The constructor and the four methods of WeakMap work the same as their Map equivalents:

  Quiz

See quiz app.