weakmap 使用场景

201 阅读5分钟

什么是 weakmap, 和 map 有什么区别

弱引用的定义

在 JavaScript 中,弱引用(Weak Reference)是一种特殊类型的引用,它不会阻止其引用的对象被垃圾回收器回收。相比于常规的强引用,弱引用并不增加被引用对象的引用计数,因此被弱引用关联的对象可以在没有其他强引用时被回收。

weakMap

WeakMap 是一种键值对的集合,其中键是弱引用,只能是对象,并且值可以是任意类型。当键对象被回收时,与之关联的值也会被自动清除。

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意。

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

  • 属性:

    • constructor:构造函数
  • 方法:

    • has(key):判断是否有 key 关联对象
    • get(key):返回key关联对象(没有则则返回 undefined)
    • set(key):设置一组key关联对象
    • delete(key):移除 key 的关联对象

区别有以下四点

引用方式:WeakMap 中的键必须是对象,而 Map 中的键可以是任意类型的值(包括对象、原始类型等)。

引用关系:在 WeakMap 中,键是弱引用,不会阻止与之关联的对象被垃圾回收,而在 Map 中,键是强引用,会阻止与之关联的对象被垃圾回收。

遍历方式:WeakMap 不支持直接遍历,没有提供类似 forEach 或 keys 方法,而 Map 可以使用 forEach、keys、values 等方法进行遍历。

大小和性能:WeakMap 没有提供类似 size 属性来获取元素数量,也没有提供 clear 方法清空所有元素。在性能方面,由于 WeakMap 的实现方式不同,对于大型数据集,WeakMap 可能比 Map 在存取速度上更高效。

weakmap的key必须是对象,而map可以是任何值
  const weakMap = new WeakMap();

    const key1 = "key1";
    const key2 = 123;
    const key3 = { name: "John" };

    // 使用非对象作为键,会引发 TypeError
    weakMap.set(key1, "value1"); // TypeError: Invalid value used as weak map key
    weakMap.set(key2, "value2"); // TypeError: Invalid value used as weak map key

    // 使用对象作为键
    weakMap.set(key3, "value3");

    console.log(weakMap.get(key3)); // 输出: value3
无法用 foreach 或者 keys() 遍历
const map = new Map();
const weakMap = new WeakMap();

const key1 = { name: 'John' };
const key2 = { name: 'Alice' };

map.set(key1, 'value1');
map.set(key2, 'value2');
weakMap.set(key1, 'value1');
weakMap.set(key2, 'value2');

// 使用 forEach 遍历 Map
map.forEach((value, key) => {
  console.log(key, value);
});
// 输出:
// { name: 'John' } value1
// { name: 'Alice' } value2

// 使用 keys 遍历 Map
for (const key of map.keys()) {
  console.log(key, map.get(key));
}
// 输出:
// { name: 'John' } value1
// { name: 'Alice' } value2

// 尝试使用 forEach 遍历 WeakMap,会抛出 TypeError
weakMap.forEach((value, key) => {
  console.log(key, value);
});
// TypeError: weakMap.forEach is not a function

// 尝试使用 keys 遍历 WeakMap,会抛出 TypeError
for (const key of weakMap.keys()) {
  console.log(key, weakMap.get(key));
}
// TypeError: weakMap.keys is not a function

场景一: 引用 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)
);

image.png

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

image.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掉。

image.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的特性,也利用了闭包。