WeakMap
Baseline 广泛可用 *
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 中获取值需要遍历所有键以查找匹配项,然后使用该匹配项的索引从值数组中检索相应的值。
这样的实现会有两个主要的不便之处:
- 第一个是不便之处在于设置和搜索操作需要 O(n) 的时间复杂度(n 是 Map 中的键的数量),因为这两个操作都必须遍历键列表以查找匹配的值。
- 第二个不便之处是内存泄漏,因为数组会永久维护对每个键和每个值的引用。这些引用会阻止键被垃圾回收,即使没有其他对该对象的引用。这也会阻止相应的值被垃圾回收。
相比之下,在 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
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 的类
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 可以被同一构造函数创建的所有实例重用,使其更节省内存,并允许同一类的不同实例读取彼此的私有成员。
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
这大致相当于使用私有字段的以下代码:
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 元素稍后可以访问这些数据。一种常见的方法是将数据附加为属性:
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!");
}
});
});
这种方法有效,但也有一些陷阱:
clicked属性是可枚举的,因此它会出现在Object.keys(button)、for...in循环等中。这可以通过使用Object.defineProperty()来缓解,但这会使代码更冗长。clicked属性是一个普通字符串属性,因此可以被其他代码访问和覆盖。这可以通过使用Symbol键来缓解,但该键仍然可以通过Object.getOwnPropertySymbols()访问。
使用 WeakMap 可以解决这些问题:
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 中移除,关联的元数据将自动被垃圾回收。
缓存
你可以将传递给函数的对象与函数的返回值关联起来,这样如果再次传递同一个对象,就可以返回缓存的结果而无需重新执行函数。如果函数是纯函数(即它不修改任何外部对象或引起其他可观察的副作用),这一点很有用。
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;
}
这只在函数的输入是对象时才有效。此外,即使输入不再被传递,只要键(输入)仍然存活,结果就会永远保留在缓存中。更有效的方法是使用一个 Map 与 WeakRef 对象配对,这允许你将任何类型的输入值与其各自(可能很大)的计算结果关联起来。有关更多详细信息,请参阅 WeakRefs 和 FinalizationRegistry 示例。
规范
| 规范 |
|---|
| ECMAScript® 2026 语言规范 # sec-weakmap-objects |
浏览器兼容性
加载中…