In this blog post, we take a look at the ECMAScript 2023 feature “Symbols as WeakMap keys” – which was proposed by Robin Ricard, Rick Button, Daniel Ehrenberg, Leo Balter, Caridy Patiño, Rick Waldron, and Ashley Claymore.
The key ability of a WeakMap is to associate data with a value:
Consider the following code:
const obj = {};
obj.attached = { data: 123 };
assert.deepEqual(
obj.attached,
{ data: 123 }
);
With a WeakMap, we can attach data without mutating obj
:
const attached = new WeakMap();
{
const obj = {};
attached.set(obj, { data: 123 });
assert.deepEqual(
attached.get(obj),
{ data: 123 }
);
}
This kind of non-mutating attaching has two main use cases:
The WeakMap holds the key weakly: If the key is garbage-collected, the whole entry is removed and the data can be garbage-collected, too (unless it is references somewhere else). That means that the illusion of the attached data being part of the value is really good and there won’t be memory leaks.
Which values can be keys in WeakMaps is documented in the ECMAScript specification, via the specification function CanBeHeldWeakly()
:
Symbol.for()
)The latter kind of key is new and was added as part of the feature “Symbols as WeakMap keys”.
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:
// 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:
symbol-refs.js
contains an API that lets us use symbols as references to values:
const refToValue = new WeakMap();
export function createRef(value) {
const ref = Symbol();
refToValue.set(ref, value);
return ref;
}
export function deref(ref) {
return refToValue.get(ref);
}
This is what using the API looks like:
import {createRef, deref} from './symbol-refs.js';
const obj = {};
const ref = createRef(obj);
assert.equal(
typeof ref, 'symbol'
);
assert.ok(
deref(ref) === obj
);
Next, we’ll use this API for the two aforementioned use cases.
records and tuples are value types (and therefore immutable) that cannot contain objects. Thanks to WeakMaps, we can use symbols as references to objects and put them inside records and tuples (example based on an idea by Robin Ricard):
import {createRef, deref} from './symbol-refs.js';
function createNode(id) {
const elemRef = createRef(document.getElementById(id));
return #{
id,
elemRef,
};
}
const node = createNode('menu-list');
assert.equal(
typeof deref(node.elemRef),
'object'
);
assert.equal(
deref(node.elemRef).id,
'menu-list'
);
Values passed into and out of ShadowRealms must be primitive or callable. That rules out objects – which are neither. Once again, we can use symbols as references to objects as a work-around:
import {createRef, deref} from './symbol-refs.js';
const otherRealm = new ShadowRealm();
const shadowIdentity = otherRealm.evaluate(
`x => x`
);
const object = { color: 'green' };
// Object identites are preserved if they are passed through
// another realm
const result = shadowIdentity(createRef(object));
assert.ok(
deref(result) === object
);
Content in “JavaScript for impatient programmers”:
Other material: