Skip to content

Commit

Permalink
WeakMap use cases with metadata and caching (mdn#22206)
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh-Cena authored Nov 10, 2022
1 parent 2254f8c commit ab532fd
Showing 1 changed file with 75 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ But because a `WeakMap` doesn't allow observing the liveness of its keys, its ke
### Using WeakMap

```js
const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap();
const o1 = {},
o2 = function() {},
o3 = window;
const wm1 = new WeakMap();
const wm2 = new WeakMap();
const wm3 = new WeakMap();
const o1 = {};
const o2 = function () {};
const o3 = window;

wm1.set(o1, 37);
wm1.set(o2, 'azerty');
Expand Down Expand Up @@ -100,23 +100,24 @@ wm1.has(o1); // false

```js
class ClearableWeakMap {
#wm;
constructor(init) {
this._wm = new WeakMap(init);
this.#wm = new WeakMap(init);
}
clear() {
this._wm = new WeakMap();
this.#wm = new WeakMap();
}
delete(k) {
return this._wm.delete(k);
return this.#wm.delete(k);
}
get(k) {
return this._wm.get(k);
return this.#wm.get(k);
}
has(k) {
return this._wm.has(k);
return this.#wm.has(k);
}
set(k, v) {
this._wm.set(k, v);
this.#wm.set(k, v);
return this;
}
}
Expand Down Expand Up @@ -197,6 +198,68 @@ thing.showPrivate();
// 1
```

### Associating metadata

A {{jsxref("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](/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain).

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:

```js
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:

- The `clicked` property is enumerable, so it will show up in [`Object.keys(button)`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys), [`for...in`](/en-US/docs/Web/JavaScript/Reference/Statements/for...in) loops, etc. This can be mitigated by using {{jsxref("Object.defineProperty()")}}, but that makes the code more verbose.
- The `clicked` property is a normal string property, so it can be accessed and overwritten by other code. This can be mitigated by using a {{jsxref("Symbol")}} key, but the key would still be accessible via {{jsxref("Object.getOwnPropertySymbols()")}}.

Using a `WeakMap` fixes these:

```js
const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
clicked.set(button, false);
buttons.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.

### Caching

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

```js
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. A more effective way is to use a {{jsxref("Map")}} paired with {{jsxref("WeakRef")}} objects, which allows you to associate any type of input value with its respective (potentially large) computation result. See the [WeakRefs and FinalizationRegistry](/en-US/docs/Web/JavaScript/Memory_Management#weakrefs_and_finalizationregistry) example for more details.

## Specifications

{{Specifications}}
Expand Down

0 comments on commit ab532fd

Please sign in to comment.