这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
前言
WeakMap是ES6引入的一个新的数据结构,和Map一样,它也是用来存储键值的映射。
相关方法
方法名和使用规则与Map一样,但是缺少了键值的遍历方法,这是因为WeakMap的键值是不可枚举的,且随时可能会被GC。
get(key)set(key, value)delete(key)has(key)
弱引用
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。from Wiki
总的来说,弱引用本质上也是指针,但是跟强引用不同,它能更好地配合垃圾回收程序的运行,因为它不会占用引用计数。
Map vs WeakMap
WeakMap 和 Map有三点重要不同:
- 键必须是一个对象
- 键是弱引用对象
WeakMap是一个黑盒,因此无法对它进行遍历和获取它的大小。
浏览器控制台是一个REPL环境,当你打印一个WeakMap实例时,依旧能够获得它其中的内容。但是Node的控制台也是REPL环境,但是却不能输出WeakMap实例的内容。
因此,如果你想要可枚举、可缓存对象的键值数据结构,你应该使用 Map。
如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用WeakMap。
使用场景
很多用Object来实现键值存储的地方都能用WeakMap来代替,并获得更好的性能。
引用DOM元素
当我们需要为一些DOM变量建立一个映射关系时,我们可以使用Map,但不能用Object,因为Object的键是字符串类型,会进行隐式类型转换。
举个栗子:
将一个按钮对象实例作为键存储在Map中,并将点击次数作为其值。
let btn1 = document.getElementById("btn");
const map = new Map();
map.set(btn1, 0);
btn1.addEventListener('click',
() => map.set(btn1, map.get(btn1) + 1)
);
一旦我们不再需要它,将它从DOM上删除,但Map依旧会持有这个按钮对象实例的引用。
即便变量btn1不再指向这个按钮对象实例,但是由于Map依旧持有这个实例的引用,因此它的引用计数为1,这会导致内存泄漏,因为这个实例不能被垃圾回收,除非我们手动解除引用。
使用WeakMap来代替Map,既能实现相同功能,又不会影响垃圾回收的正常进行。
let btn1 = document.getElementById("btn");
const map = new WeakMap();
map.set(btn1, 0);
btn1.addEventListener('click',
() => map.set(btn1, map.get(btn1) + 1)
);
WeakMap持有的是这个按钮对象实例的弱引用,不会占用它的引用计数,因此这个按钮从DOM上删除或不被任何变量引用时,便被GC掉。
实现对象缓存
弱引用还可以用来实现缓存。例如用弱哈希表,即通过弱引用来缓存各种引用对象的哈希表。当垃圾回收器运行时,假如应用程序的内存占用量高到一定程度,那些不再被其它对象所引用的缓存对象就会被自动释放。
const cache = new WeakMap();
// basket是一个数组
function computeTotalPrice(basket) {
if (cache.has(basket)) {
return cache.get(basket);
} else {
let total = 0;
basket.forEach(itemPrice => total += itemPrice)
cache.set(basket, total)
return total;
}
}
检测对象的循环引用
这其实也是对象缓存的一种。下面是一个对象深克隆方法的栗子,只是一个简单实现。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 获取当前值的构造函数:获取它的类型
let constructor = target.constructor;
// 检测当前对象target是否与正则、日期格式对象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 创建一个新的特殊对象(正则类/日期类)的实例
return new constructor(target);
}
if (isObject(target)) {
map.set(target, true); // 为循环引用的对象做标记
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
实现类的私有变量
目前,js中的类的熟悉和方法都是公有的,但tc39中有个关于类的私有变量的草案:tc39/proposal-class-fields,未来有可能成为标准。
现在,我们可以利用WeakMap是一个黑盒的特性,来实现类的私有变量:
let _data = new WeakMap();
class Registry {
constructor(person, action) {
const data = []
_data.set(this, data);
}
get() {
return _data.get(this)
}
set(person, action){
const data = _data.get(this)
data.push([person,action])
_data.set(this, data);
}
}
这里不仅利用了WeakMap的特性,也利用了闭包。
总结
综上,利用WeakMap的键是弱引用和不可枚举的特性,我们能更好地优化程序的内存使用,以及实现一些hack功能。