WeakMap

WeakMap 是键/值对的集合,其键必须是对象或 非注册符号,具有任意 JavaScript 类型 的值,并且不会创建对其键的强引用。也就是说,对象作为 WeakMap 中的键的存在不会阻止该对象被垃圾收集。一旦用作键的对象被收集,它在任何 WeakMap 中的对应值也将成为垃圾收集的候选者 - 只要它们在其他地方没有被强烈引用。唯一可以用作 WeakMap 键的原始类型是符号(更具体地说,是 非注册符号),因为未注册的符号保证是唯一的并且无法重新创建。

¥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 允许以不妨碍收集关键对象的方式将数据与对象关联,即使值引用了键也是如此。然而,WeakMap 不允许观察其键的活跃度,这就是它不允许枚举的原因;如果 WeakMap 暴露任何方法来获取其键列表,则该列表将取决于垃圾收集的状态,从而引入非确定性。如果你想要一个键列表,你应该使用 Map 而不是 WeakMap

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

你可以在 键控集合 指南的 弱映射对象 部分了解有关 WeakMap 的更多信息。

¥You can learn more about WeakMap in the WeakMap object section of the Keyed collections guide.

描述

¥Description

WeakMap 的键必须是可垃圾回收的。大多数 primitive data types 可以任意创建并且没有生命周期,因此它们不能用作密钥。对象和 非注册符号 可以用作键,因为它们是垃圾可收集的。

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

为什么是 WeakMap?

¥Why WeakMap?

地图 API 可以在 JavaScript 中实现,其中包含由四种 API 方法共享的两个数组(一个用于键,一个用于值)。在此映射上设置元素需要同时将键和值推送到每个数组的末尾。因此,键和值的索引将对应于两个数组。从映射中获取值需要迭代所有键以查找匹配项,然后使用该匹配项的索引从值数组中检索相应的值。

¥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:

  1. 第一个是 O(n) 集和搜索(n 是映射中键的数量),因为两个操作都必须迭代键列表以查找匹配值。
  2. 第二个不便之处是内存泄漏,因为数组确保无限期地维护对每个键和每个值的引用。这些引用可以防止键被垃圾回收,即使没有其他对该对象的引用。这也会阻止相应的值被垃圾收集。

相比之下,在 WeakMap 中,只要密钥没有被垃圾回收,密钥对象就会强引用其内容,但从那时起就弱引用。因此,WeakMap

¥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 以外的其他地方引用,则允许对任何值进行垃圾回收

当将密钥映射到有关该密钥的信息时,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.

但由于 WeakMap 不允许观察其键的活跃度,因此其键不可枚举。没有方法可以获取密钥列表。如果有的话,该列表将取决于垃圾收集的状态,从而引入非确定性。如果你想要一个键列表,你应该使用 Map

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

构造函数

¥Constructor

WeakMap()

创建一个新的 WeakMap 对象。

实例属性

¥Instance properties

这些属性在 WeakMap.prototype 上定义并由所有 WeakMap 实例共享。

¥These properties are defined on WeakMap.prototype and shared by all WeakMap instances.

WeakMap.prototype.constructor

创建实例对象的构造函数。对于 WeakMap 实例,初始值为 WeakMap 构造函数。

WeakMap.prototype[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值为字符串 "WeakMap"。该属性在 Object.prototype.toString() 中使用。

实例方法

¥Instance methods

WeakMap.prototype.delete()

删除与 key 关联的任何值。之后 WeakMap.prototype.has(key) 将返回 false

WeakMap.prototype.get()

返回与 key 关联的值,如果没有,则返回 undefined

WeakMap.prototype.has()

返回一个布尔值,断言某个值是否已与 WeakMap 对象中的 key 关联。

WeakMap.prototype.set()

设置 valueWeakMap 对象中的 key。返回 WeakMap 对象。

示例

¥Examples

使用 WeakMap

¥Using WeakMap

js
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");
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

使用 .clear() 方法实现类似 WeakMap 的类

¥Implementing a WeakMap-like class with a .clear() method

js
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

开发者可以使用 WeakMap 将私有数据关联到对象,具有以下好处:

¥Developers can use a WeakMap to associate private data to an object, with the following benefits:

  • Map 相比,WeakMap 不持有对用作键的对象的强引用,因此元数据与对象本身共享相同的生命周期,从而避免内存泄漏。
  • 与使用不可枚举和/或 Symbol 属性相比,WeakMap 位于对象外部,用户代码无法通过 Object.getOwnPropertySymbols 等反射方法检索元数据。
  • closure 相比,同一个 WeakMap 可以重用于从构造函数创建的所有实例,从而提高内存效率,并允许同一类的不同实例相互读取彼此的私有成员。
js
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:

js
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

WeakMap 可用于将元数据与对象关联,而不影响对象本身的生命周期。这与私有成员示例非常相似,因为私有成员也被建模为不参与 原型继承 的外部元数据。

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

该用例可以扩展到已经创建的对象。例如,在网络上,我们可能希望将额外的数据与 DOM 元素关联起来,DOM 元素稍后可以访问这些数据。一种常见的方法是将数据附加为属性:

¥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:

使用 WeakMap 可以解决以下问题:

¥Using a WeakMap fixes these:

js
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!");
    }
  });
});

这里,只有有权访问 clicked 的代码才知道每个按钮的单击状态,外部代码无法修改状态。此外,如果任何按钮从 DOM 中删除,相关元数据将自动被垃圾收集。

¥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;
}

仅当函数的输入是对象时,这才有效。此外,即使输入不再传入,只要键(输入)有效,结果仍将永远保留在缓存中。更有效的方法是使用 MapWeakRef 对象配对,这允许你将任何类型的输入值与其各自(可能很大)的计算结果相关联。有关详细信息,请参阅 WeakRefs 和 FinalizationRegistry 示例。

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

规范

Specification
ECMAScript Language Specification
# sec-weakmap-objects

¥Specifications

浏览器兼容性

BCD tables only load in the browser

¥Browser compatibility

也可以看看