WeakMap

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

* 此特性的某些部分可能存在不同级别的支持。

WeakMap 是一种键值对集合,其键必须是对象或非注册 Symbol,值为任意 JavaScript 类型,并且它不会对其键创建强引用。也就是说,一个对象作为 WeakMap 的键,并不会阻止该对象被垃圾回收。一旦用作键的对象被回收,它在任何 WeakMap 中的对应值也就成为垃圾回收的候选对象,前提是它们没有被其他地方强引用。唯一可以用作 WeakMap 键的原始类型是 Symbol——更具体地说,是非注册 Symbol——因为非注册 Symbol 被保证是唯一的,并且不能被重新创建。

WeakMap 允许以一种不阻止键对象被回收的方式将数据与对象关联起来,即使值引用了键。但是,WeakMap 不允许观察其键的存活性,这也是它不允许枚举的原因;如果 WeakMap 暴露任何获取其键列表的方法,该列表将取决于垃圾回收的状态,从而引入了不确定性。如果你需要键的列表,你应该使用 Map 而不是 WeakMap

你可以在 键值集合 指南的 WeakMap 对象 部分了解更多关于 WeakMap 的信息。

描述

WeakMap 的键必须是可被垃圾回收的。大多数 原始数据类型都可以任意创建,并且没有生命周期,因此不能用作键。对象和 非注册 Symbol 可以用作键,因为它们是可被垃圾回收的。

键的相等性

与常规 Map 一样,值相等基于 SameValueZero 算法,这与 === 运算符相同,因为 WeakMap 只能包含对象和 Symbol 键。这意味着对于对象键,相等基于对象标识。它们是通过 引用而不是值进行比较的。

为什么使用 WeakMap?

可以使用两个数组(一个用于键,一个用于值)来在 JavaScript 中实现一个 Map API,这四个 API 方法共享这两个数组。向这个 Map 设置元素需要同时将键和值推到这两个数组的末尾。结果是,键和值的索引将对应于两个数组。从 Map 中获取值需要遍历所有键以查找匹配项,然后使用该匹配项的索引从值数组中检索相应的值。

这样的实现会有两个主要的不便之处:

  1. 第一个是不便之处在于设置和搜索操作需要 O(n) 的时间复杂度(n 是 Map 中的键的数量),因为这两个操作都必须遍历键列表以查找匹配的值。
  2. 第二个不便之处是内存泄漏,因为数组会永久维护对每个键和每个值的引用。这些引用会阻止键被垃圾回收,即使没有其他对该对象的引用。这也会阻止相应的值被垃圾回收。

相比之下,在 WeakMap 中,一个键对象只要该键没有被垃圾回收,就会对其内容强引用,但之后则弱引用。因此,WeakMap

  • 不会阻止垃圾回收,从而最终移除对键对象的引用
  • 如果键对象除了 WeakMap 之外没有其他引用,则允许垃圾回收任何值

当将键映射到仅在键未被垃圾回收时才具有价值的信息时,WeakMap 可以是一个特别有用的结构。

但是,由于 WeakMap 不允许观察其键的存活性,因此其键是不可枚举的。没有方法可以获取键的列表。如果存在,列表将取决于垃圾回收的状态,从而引入了不确定性。如果你需要键的列表,你应该使用 Map

构造函数

WeakMap()

创建一个新的 WeakMap 对象。

实例属性

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

WeakMap.prototype.constructor

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

WeakMap.prototype[Symbol.toStringTag]

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

实例方法

WeakMap.prototype.delete()

从此 WeakMap 中移除由键指定的条目。

WeakMap.prototype.get()

返回此 WeakMap 中与键对应的value,如果不存在则返回 undefined

WeakMap.prototype.getOrInsert() 实验性

返回此 WeakMap 中与指定键对应的 value。如果键不存在,则使用给定的默认值插入新条目,并返回插入的值。

WeakMap.prototype.getOrInsertComputed() 实验性

返回此 WeakMap 中与指定键对应的 value。如果键不存在,则使用给定回调函数计算出的默认值插入新条目,并返回插入的值。

WeakMap.prototype.has()

返回一个布尔值,指示此 WeakMap 中是否存在具有指定键的条目。

WeakMap.prototype.set()

向此 WeakMap 添加具有指定键和值的条目,如果键已存在,则更新现有条目。

示例

使用 WeakMap

js
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

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

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

模拟私有成员

开发人员可以使用 WeakMap 将私有数据与对象关联起来,具有以下优点:

  • Map 相比,WeakMap 不持有对用作键的对象本身的强引用,因此元数据与对象本身具有相同的生命周期,避免了内存泄漏。
  • 与使用非枚举和/或 Symbol 属性相比,WeakMap 存在于对象外部,并且用户代码无法通过反射方法(如 Object.getOwnPropertySymbols)检索元数据。
  • 闭包相比,同一个 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

这大致相当于使用私有字段的以下代码:

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

关联元数据

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

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

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

这种方法有效,但也有一些陷阱:

使用 WeakMap 可以解决这些问题:

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 中移除,关联的元数据将自动被垃圾回收。

缓存

你可以将传递给函数的对象与函数的返回值关联起来,这样如果再次传递同一个对象,就可以返回缓存的结果而无需重新执行函数。如果函数是纯函数(即它不修改任何外部对象或引起其他可观察的副作用),这一点很有用。

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 示例。

规范

规范
ECMAScript® 2026 语言规范
# sec-weakmap-objects

浏览器兼容性

另见