Baseline Widely available *
A WeakMap
is a collection of key/value pairs whose keys must be objects or non-registered symbols, with values of any arbitrary JavaScript type, and which does not create strong references to its keys. That is, an object's presence as a key in a WeakMap
does not prevent the object from being garbage collected. Once an object used as a key has been collected, its corresponding values in any WeakMap
become candidates for garbage collection as well â as long as they aren't strongly referred to elsewhere. The only primitive type that can be used as a WeakMap
key is symbol â more specifically, non-registered symbols â because non-registered symbols are guaranteed to be unique and cannot be re-created.
WeakMap
allows associating data to objects in a way that doesn't prevent the key objects from being collected, even if the values reference the keys. However, a WeakMap
doesn't allow observing the liveness of its keys, which is why it doesn't allow enumeration; if a WeakMap
exposed any method to obtain a list of its keys, the list would depend on the state of garbage collection, introducing non-determinism. If you want to have a list of keys, you should use a Map
rather than a WeakMap
.
You can learn more about WeakMap
in the WeakMap object section of the Keyed collections guide.
Keys of WeakMaps must be garbage-collectable. Most primitive data types can be arbitrarily created and don't have a lifetime, so they cannot be used as keys. Objects and non-registered symbols can be used as keys because they are garbage-collectable.
Why WeakMap?A map API could be implemented in JavaScript with two arrays (one for keys, one for values) shared by the four API methods. Setting elements on this map would involve pushing a key and value onto the end of each of those arrays simultaneously. As a result, the indices of the key and value would correspond to both arrays. Getting values from the map would involve iterating through all keys to find a match, then using the index of this match to retrieve the corresponding value from the array of values.
Such an implementation would have two main inconveniences:
O(n)
set and search (n being the number of keys in the map) since both operations must iterate through the list of keys to find a matching value.By contrast, in a WeakMap
, a key object refers strongly to its contents as long as the key is not garbage collected, but weakly from then on. As such, a WeakMap
:
WeakMap
A WeakMap
can be a particularly useful construct when mapping keys to information about the key that is valuable only if the key has not been garbage collected.
But because a WeakMap
doesn't allow observing the liveness of its keys, its keys are not enumerable. There is no method to obtain a list of the keys. If there were, the list would depend on the state of garbage collection, introducing non-determinism. If you want to have a list of keys, you should use a Map
.
WeakMap()
Creates a new WeakMap
object.
These properties are defined on WeakMap.prototype
and shared by all WeakMap
instances.
WeakMap.prototype.constructor
The constructor function that created the instance object. For WeakMap
instances, the initial value is the WeakMap
constructor.
WeakMap.prototype[Symbol.toStringTag]
The initial value of the [Symbol.toStringTag]
property is the string "WeakMap"
. This property is used in Object.prototype.toString()
.
WeakMap.prototype.delete()
Removes any value associated to the key
. WeakMap.prototype.has(key)
will return false
afterwards.
WeakMap.prototype.get()
Returns the value associated to the key
, or undefined
if there is none.
WeakMap.prototype.has()
Returns a Boolean asserting whether a value has been associated to the key
in the WeakMap
object or not.
WeakMap.prototype.set()
Sets the value
for the key
in the WeakMap
object. Returns the WeakMap
object.
const wm1 = new WeakMap();
const wm2 = new WeakMap();
const wm3 = new WeakMap();
const o1 = {};
const o2 = () => {};
const o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // a value can be anything, including an object or a function
wm2.set(o2, undefined);
wm2.set(wm1, wm2); // keys and values can be any objects. Even WeakMaps!
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, because that is the set value
wm2.get(o3); // undefined, because there is no key for o3 on wm2
wm1.has(o2); // true
wm2.has(o2); // true (even if the value itself is 'undefined')
wm2.has(o3); // false
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
Implementing a WeakMap-like class with a .clear() method
class ClearableWeakMap {
#wm;
constructor(init) {
this.#wm = new WeakMap(init);
}
clear() {
this.#wm = new WeakMap();
}
delete(k) {
return this.#wm.delete(k);
}
get(k) {
return this.#wm.get(k);
}
has(k) {
return this.#wm.has(k);
}
set(k, v) {
this.#wm.set(k, v);
return this;
}
}
Emulating private members
Developers can use a WeakMap
to associate private data to an object, with the following benefits:
Map
, a WeakMap does not hold strong references to the object used as the key, so the metadata shares the same lifetime as the object itself, avoiding memory leaks.Symbol
properties, a WeakMap is external to the object and there is no way for user code to retrieve the metadata through reflective methods like Object.getOwnPropertySymbols
.let Thing;
{
const privateScope = new WeakMap();
let counter = 0;
Thing = function () {
this.someProperty = "foo";
privateScope.set(this, {
hidden: ++counter,
});
};
Thing.prototype.showPublic = function () {
return this.someProperty;
};
Thing.prototype.showPrivate = function () {
return privateScope.get(this).hidden;
};
}
console.log(typeof privateScope);
// "undefined"
const thing = new Thing();
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
This is roughly equivalent to the following, using private fields:
class Thing {
static #counter = 0;
#hidden;
constructor() {
this.someProperty = "foo";
this.#hidden = ++Thing.#counter;
}
showPublic() {
return this.someProperty;
}
showPrivate() {
return this.#hidden;
}
}
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
Associating metadata
A WeakMap
can be used to associate metadata with an object, without affecting the lifetime of the object itself. This is very similar to the private members example, since private members are also modelled as external metadata that doesn't participate in prototypical inheritance.
This use case can be extended to already-created objects. For example, on the web, we may want to associate extra data with a DOM element, which the DOM element may access later. A common approach is to attach the data as a property:
const buttons = document.querySelectorAll(".button");
buttons.forEach((button) => {
button.clicked = false;
button.addEventListener("click", () => {
button.clicked = true;
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => button.clicked)) {
console.log("All buttons have been clicked!");
}
});
});
This approach works, but it has a few pitfalls:
clicked
property is enumerable, so it will show up in Object.keys(button)
, for...in
loops, etc. This can be mitigated by using Object.defineProperty()
, but that makes the code more verbose.clicked
property is a normal string property, so it can be accessed and overwritten by other code. This can be mitigated by using a Symbol
key, but the key would still be accessible via Object.getOwnPropertySymbols()
.Using a WeakMap
fixes these:
const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
clicked.set(button, false);
button.addEventListener("click", () => {
clicked.set(button, true);
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => clicked.get(button))) {
console.log("All buttons have been clicked!");
}
});
});
Here, only code that has access to clicked
knows the clicked state of each button, and external code can't modify the states. In addition, if any of the buttons gets removed from the DOM, the associated metadata will automatically get garbage-collected.
You can associate objects passed to a function with the result of the function, so that if the same object is passed again, the cached result can be returned without re-executing the function. This is useful if the function is pure (i.e., it doesn't mutate any outside objects or cause other observable side effects).
const cache = new WeakMap();
function handleObjectValues(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = Object.values(obj).map(heavyComputation);
cache.set(obj, result);
return result;
}
This only works if your function's input is an object. Moreover, even if the input is never passed in again, the result still remains forever in the cache as long as the key (input) is alive. A more effective way is to use a Map
paired with WeakRef
objects, which allows you to associate any type of input value with its respective (potentially large) computation result. See the WeakRefs and FinalizationRegistry example for more details.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4