🔄 JavaScript 弱映射与弱集合
前述 垃圾回收 中,JavaScript 引擎在值「可达」和可能被使用时会将其保持在内存中。通常,当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的。
🌰 例子 / 如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。
let john = { name: "John" };
let array = [ john ];
john = null
2
3
4
5
前面由
john
所引用的那个对象被存储在了array
中,所以它不会被垃圾回收机制回收,可以通过array[0]
获取到它。
🌰 例子 / 类似上例子,如果使用对象作为常规 Map
的键,那么当 Map
存在时,该对象也将存在。它会占用内存,并且应该不会被(垃圾回收机制)回收。
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
2
3
4
5
6
john
被存储在了map
中,可以使用map.keys()
来获取它。
# 弱映射 WeakMap
前面介绍垃圾回收的机制,就是说明 Map 可以保留对象作为键不被垃圾回收。 WeakMap
在这方面有着 根本上 的不同,它 不会阻止垃圾回收机制对作为键的对象(key object)的回收。
第一个不同点: WeakMap
的键 必须是对象,不能是原始值:
🌰 例子:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok");
// 不能使用字符串作为键
weakMap.set("test", "Whoops"); // Error
2
3
4
5
6
7
8
现在,如果 weakMap
中使用一个对象作为键,并且没有其他对这个对象的引用,该对象将会被从内存(和 map)中自动清除。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null;
2
3
4
5
6
与上面常规的
Map
的例子相比,现在如果john
仅仅是作为WeakMap
的键而存在,它将会被从 map(和内存)中自动删除。
WeakMap
不支持迭代以及 keys()
, values()
和 entries()
方法。所以没有办法获取 WeakMap
的所有键或值。
🍎 WeakMap
只有以下的方法:
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
WeakMap
存在的意义:如果一个对象丢失了其它所有引用(就像上面例子中的 john
),那么它就会被垃圾回收机制自动回收。但是在从技术的角度并不能准确知道 何时会被回收。
这些都是由 JavaScript 引擎决定的。JavaScript 引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么 JavaScript 引擎可能就会选择等一等,稍后再进行内存清理。因此,从技术上讲,
WeakMap
的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问WeakMap
的所有键 / 值的方法。
# 弱映射的应用实例
主要应用场景:额外数据的存储。
如果正在处理一个「属于」另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡,
WeakMap
就可以派上用场。
将这项数据放进 WeakMap
中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。
weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 将会被自动清除
2
# 用户访问计数
🌰 例子 / 如果有用于处理用户访问计数的代码。收集到的信息被存储在 map
中:一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时这些数据不再需要。
📃 在处理用户计数 visitCount.js
中:
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
2
3
4
5
6
7
📃 在调用 countUser
的文件中:
let john = { name: "John" };
countUser(john); // count his visits
// 不久之后,john 离开了
john = null;
2
3
4
5
现在,
john
这个对象应该被垃圾回收。当john
对象变成不可达时,即便它是WeakMap
里的一个键,它也会连同它作为WeakMap
里的键所对应的信息一同被从内存中删除。
# 缓存
存储(“缓存”)函数的结果,以便将来对同一个对象的调用可以重用这个结果。对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用可以直接从 cache
中获取。这样做的缺点是,当我们不再需要这个对象的时候需要清理 cache
。
如果我们用 WeakMap
替代 Map
,便不会存在这个问题。当对象被垃圾回收时,对应缓存的结果也会被自动从内存中清除。
📁 在 cache.js
中:
let cache = new WeakMap();
// 计算并记结果
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// 不再需要这个对象时:
obj = null;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注意使用
WeakMap
后无法获取cache.size
。当 obj 被垃圾回收,缓存的数据也会被清除
# 弱集合 WeakSet
WeakSet
的表现类似:
- 与
Set
类似,但是只能向WeakSet
添加对象(而且不能是原始值)。 - 对象只有在其它某个(些)地方能被访问的时候,才能留在
set
中。 - 跟
Set
一样,WeakSet
支持add
,has
和delete
方法,但不支持size
和keys()
,并且不可迭代。
Set
变为弱集合 WeakSet
的同时,可以作为额外的存储空间。但并非针对任意数据,而是针对「是 / 否『的事实。 WeakSet
的元素可能代表着有关该对象的某些信息。
🌰 例子 / 可以将用户添加到 WeakSet
中,以追踪访问过我们网站的用户:
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John 访问了我们
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次访问
console.log(visitedSet.has(john)) // true
console.log(visitedSet(mary)) // false
john = null // 离开
// visitedSet 将被自动清理(即自动清除其中已失效的值 john)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 总结
WeakMap
和WeakSet
最明显的局限性就是不能迭代,并且无法获取所有当前内容。那样可能会造成不便,但是并不会阻止WeakMap
/WeakSet
完成其主要工作,成为在其它地方管理 / 存储「额外」的对象数据。- 主要优点是它们对对象是弱引用,所以被它们引用的对象很容易地被垃圾收集器移除。
WeakMap
是类似于Map
的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。WeakSet
是类似于Set
的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。WeakMap
和WeakSet
被用作「主要」对象存储之外的「辅助」数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作WeakMap
或WeakSet
的键,那么它将被自动清除。