深入了解WeakMap

1,603 阅读4分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

前言

WeakMapES6引入的一个新的数据结构,和Map一样,它也是用来存储键值的映射。

相关方法

方法名和使用规则与Map一样,但是缺少了键值的遍历方法,这是因为WeakMap的键值是不可枚举的,且随时可能会被GC

  • get(key)
  • set(key, value)
  • delete(key)
  • has(key)

弱引用

在计算机程序设计中,弱引用强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。from Wiki

总的来说,弱引用本质上也是指针,但是跟强引用不同,它能更好地配合垃圾回收程序的运行,因为它不会占用引用计数。

Map vs WeakMap

WeakMapMap有三点重要不同:

  • 键必须是一个对象
  • 键是弱引用对象
  • WeakMap 是一个黑盒,因此无法对它进行遍历和获取它的大小。

8-7-1.png

浏览器控制台是一个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依旧会持有这个按钮对象实例的引用。

8-7-2.png

即便变量btn1不再指向这个按钮对象实例,但是由于Map依旧持有这个实例的引用,因此它的引用计数为1,这会导致内存泄漏,因为这个实例不能被垃圾回收,除非我们手动解除引用。

8-7-3.png

使用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掉。

8-7-4.png

实现对象缓存

弱引用还可以用来实现缓存。例如用弱哈希表,即通过弱引用来缓存各种引用对象的哈希表。当垃圾回收器运行时,假如应用程序的内存占用量高到一定程度,那些不再被其它对象所引用的缓存对象就会被自动释放。

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功能。