WeakMap and WeakSet(弱映射和弱集合)
我们从前面的 垃圾回收 章节中知道,JavaScript 引擎在值可访问(并可能被使用)时将其存储在内存中。
例如:
let john = { name: "John" };
// 该对象能被访问,john 是它的引用
// 覆盖引用
john = null;
// 该对象将会被从内存中清除
通常,当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的。
例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。
就像这样:
let john = { name: "John" };
let array = [ john ];
john = null; // 覆盖引用
// 前面由 john 所引用的那个对象被存储在了 array 中
// 所以它不会被垃圾回收机制回收
类似的,如果我们使用对象作为常规 Map
的键,那么当 Map
存在时,该对象也将存在。它会占用内存,并且应该不会被(垃圾回收机制)回收。
例如:
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它
WeakMap
在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收。
让我们通过例子来看看这指的到底是什么。
WeakMap
WeakMap
和 Map
的第一个不同点就是,WeakMap
的键必须是对象,不能是原始值:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // 正常工作(以对象作为键)
// 不能使用字符串作为键
weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象
现在,如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆盖引用
// john 被从内存中删除了!
与上面常规的 Map
的例子相比,现在如果 john
仅仅是作为 WeakMap
的键而存在 —— 它将会被从 map(和内存)中自动删除。
WeakMap
不支持迭代以及 keys()
,values()
和 entries()
方法。所以没有办法获取 WeakMap
的所有键或值。
WeakMap
只有以下的方法:
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
为什么会有这种限制呢?这是技术的原因。如果一个对象丢失了其它所有引用(就像上面示例中的 john
),那么它就会被垃圾回收机制自动回收。但是在从技术的角度并不能准确知道 何时会被回收。
这些都是由 JavaScript 引擎决定的。JavaScript 引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么 JavaScript 引擎可能就会选择等一等,稍后再进行内存清理。因此,从技术上讲,WeakMap
的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问 WeakMap
的所有键/值的方法。
那么,在哪里我们会需要这样的数据结构呢?
使用案例:额外的数据
WeakMap
的主要应用场景是 额外数据的存储。
假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候 WeakMap
正是我们所需要的利器。
我们将这些数据放到 WeakMap
中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。
weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 将会被自动清除
让我们来看一个例子。
例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在 map 中:一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了。
下面是一个使用 Map
的计数函数的例子:
//