WeakMap
) [ES6] (advanced)
WeakMaps are similar to Maps, with the following differences:
The next two sections examine in more detail what that means.
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.
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).
Which values can be keys in WeakMaps is documented in the ECMAScript specification, via the specification function CanBeHeldWeakly()
:
Symbol.for()
)
All kinds of keys have one thing in common – they have identity semantics:
===
, two keys are considered equal if they have the same identity – they are not compared by comparing their contents (their values). That means there are never two or more different keys (“different” meaning “at different locations in memory”) that are all considered equal. Each key is unique.
Both conditions are important so that WeakMaps can dispose entries when keys disappear and no memory leaks.
Let’s look at examples:
Non-registered symbols can be used as WeakMap keys: They are primitive but they are compared by identity and they are garbage-collected.
The following two kinds of values cannot be used as WeakMap keys:
// Get a symbol from the registry
const mySymbol = Symbol.for('key-in-symbol-registry');
assert.equal(
// Retrieve that symbol again
Symbol.for('key-in-symbol-registry'),
mySymbol
);
Symbols as WeakMap keys solve important issues for upcoming JavaScript features:
We can use WeakMaps 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';
}
With WeakMaps, we 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
, we 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']
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
WeakMap
The constructor and the four methods of WeakMap
work the same as their Map
equivalents:
new WeakMap<K, V>(entries?: Iterable<[K, V]>)
[ES6]
WeakMap.prototype.delete(key: K) : boolean
[ES6]
WeakMap.prototype.get(key: K) : V
[ES6]
WeakMap.prototype.has(key: K) : boolean
[ES6]
WeakMap.prototype.set(key: K, value: V) : this
[ES6]