WeakMap
一个WeakMap
是键值对的集合,其键必须是对象或未注册的符号,值可以是任何任意JavaScript 类型,并且不会创建对其键的强引用。也就是说,对象作为WeakMap
中的键的存在不会阻止该对象被垃圾回收。一旦用作键的对象被回收,其在任何WeakMap
中的相应值也会成为垃圾回收的候选对象——只要它们在其他地方没有被强引用。唯一可以作为WeakMap
键使用的原始类型是符号——更具体地说,是未注册的符号——因为未注册的符号保证是唯一的,并且无法重新创建。
WeakMap
允许以不阻止键对象被回收的方式将数据与对象关联,即使值引用了键。但是,WeakMap
不允许观察其键的生命周期,这就是为什么它不允许枚举的原因;如果WeakMap
公开了任何获取其键列表的方法,则该列表将取决于垃圾回收的状态,从而引入不确定性。如果你想要一个键列表,你应该使用Map
而不是WeakMap
。
你可以在WeakMap 对象部分的键控集合指南中了解有关WeakMap
的更多信息。
描述
为什么使用 WeakMap?
可以使用两个数组(一个用于键,一个用于值)在 JavaScript 中实现类似映射的 API,这两个数组由四个 API 方法共享。在该映射上设置元素将涉及同时将键和值推送到每个数组的末尾。因此,键和值的索引将对应于这两个数组。从映射中获取值将涉及遍历所有键以找到匹配项,然后使用此匹配项的索引从值数组中检索相应的值。
这种实现将有两个主要不便之处
- 第一个是
O(n)
集合和搜索(n是映射中的键数),因为这两个操作都必须遍历键列表以找到匹配的值。 - 第二个不便之处是内存泄漏,因为数组确保无限期地维护对每个键和每个值的引用。这些引用阻止了键被垃圾回收,即使对象没有其他引用也是如此。这也会阻止相应的值得以垃圾回收。
相比之下,在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()
-
删除与
key
关联的任何值。之后,WeakMap.prototype.has(key)
将返回false
。 WeakMap.prototype.get()
-
返回与
key
关联的值,如果不存在则返回undefined
。 WeakMap.prototype.has()
-
返回一个布尔值,表示
WeakMap
对象中是否已将值与key
关联。 WeakMap.prototype.set()
-
在
WeakMap
对象中为key
设置value
。返回WeakMap
对象。
示例
使用 WeakMap
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 的类
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 语言规范 # sec-weakmap-objects |
浏览器兼容性
BCD 表格仅在浏览器中加载
另请参阅
core-js
中WeakMap
的 polyfill- 键控集合
- 使用 ECMAScript 6 WeakMaps 隐藏实现细节 作者:Nick Fitzgerald (2014)
Map
Set
WeakSet