跳到主要内容

ES6+ WeakMap

1. 前言

前面我们已经学了 Set 和对应的 WeakSet,Map 对应也有 WeakMap,在学习 WeakSet 时我们已经接触到弱引的相关知识,本节我们将结合 WeakMap 深入的理解弱引用的相关问题。

由前面学到的 Set 和 WeakSet 具有很多相似的地方,比如它们存放的都是独一无二的元素。所以,对 Map 和 WeakMap 也可以进行类比,WeakMap 中也存放的是键值对。不同的是 WeakMap 的 key 只能是对象,值可以是任意类型的,和 WeakSet 一样 WeakMap 对 key 的引用是弱引用。

2. WeakMap 基本用法

WeakMap 像 Map 一样可以接受一个二维数组进行初始化。

var wm = new WeakMap([
[{name: 'imooc'}, 'imooc'],
[{name: 'lesson'}, 'ES6 Wiki']
])
console.log(wm)

上面的代码打印结果如下:

图片描述

从打印的结果可以大概了解 WeakMap 的存储方式,WeakMap 的实例本来就是一个对象。

WeakMap 只提供了四个方法用于操作数据。

方法名描述
set接收键值对,向 WeakMap 实例中添加元素
get传入指定的 key 获取 WeakMap 实例上的值
has传入指定的 key 查找在 WeakMap 实例中是否存在
delete传入指定的 key 删除 WeakMap 实例中对应的值

看如下实例:

var wm1 = new WeakMap();
var wm2 = new WeakMap();
var wm3 = new WeakMap();

var o1 = {name: 'imooc'};
var o2 = function(){};
var o3 = window;

// 使用 set 方法添加元素,value 可以是任意值,包括对象、函数甚至另外一个WeakMap对象
wm1.set(o1, 'ES6 Wiki');
wm1.set(o2, 10);
wm2.set(o1, o2);
wm2.set(o3, null);
wm2.set(wm1, wm2);

wm1.get(o2); // 10
wm2.get(o2); // undefined,wm2 中没有 o2 这个键
wm2.get(o3); // null

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是null)

wm3.set(o1, 'lesson is ES6 Wiki!');
wm3.get(o1); // lesson is ES6 Wiki!

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

上面的实例基本涵盖了 WeakMap 四种方法的基本使用情况,上面也提到了 WeakMap 的 key 只能是对象类型的,如果 WeakMap 的 key 是基本类型数据时就会报错。

var wm = new WeakMap();
wm.set('lesson', 'ES6 Wiki');
// Uncaught TypeError: Invalid错误value used as weak map key

上面代码中在设置 wm 值时,报错了。从报错类型知道是一个类型错误,弱引用映射的键是无效的。

3. WeakMap 使用场景

上节我们学习了 Map 的使用,在 JavaScript 中对对象的引用都是强保留的,这意味着只要持有该对象的引用,垃圾回收机制就不会回收该对象。

var obj = {a: 10, b: 88};

上面是一个字面量对象,只要我们访问 obj 对象,或者任何地方有引用该对象,这个对象就不会被垃圾回收。而在 ES6 之前 JavaScript 中没有弱引用概念,弱引用的本质上就是不会影响垃圾回收机制。其实,WeakMap 并不是真正意义上的弱引用,只要键仍然存在,它就强引用其上的内容。WeakMap 仅在键被垃圾回收之后,才弱引用它的内容,所以也不用太纠结其中的弱。

在官方上对为什么使用 WeakMap 做了描述,Map 在存储值是有顺序的,这种顺序是通过二维数组的形式来完成的。我们知道 Map 在初始化时接受一个数组,数组中的每一项也是一个数组,这个数组中包含两个值,一个存放的是键,一个存放的是值。新添加的值会添加到数组的末尾,从而使得键值具有索引的含义。在取值时就需要进行遍历,通过索引取出对应的值。

但是这样存在两个很大的缺陷:

  1. 赋值和搜索的时间复杂度都是 O (n) (n 是键值对的个数),因为这两个操作都是要遍历整个数组才能完成的;
  2. 可能会导致内存泄漏,因为数组会一直引用每个键和值。这种引用使得垃圾回收算法不能回收处理它们,即使没有任何引用存在。

相比之下,原生的 WeakMap 持有的是 “弱引用”,这意味着它不会影响垃圾回收。WeakMap 中的 key 只有在键值存在的情况才会引用,而且只是一个读取操作,并不会对引用的值产生影响。也正因为这样的弱引用关系,导致 WeakMap 中的 key 是不可枚举的,假设 key 是可枚举的,就会对该值产生引用关系,影响垃圾回收。

如果只是单纯地向对象上添加值用于检查某些逻辑判断,又不想影响垃圾回收机制,这个时候就可以使用 WeakMap。这里说一点,在一些框架中已经使用了像 WeakMap 和 WeakSet 这样的数据结构,其中 Vue3 就引入了这样的新数据进行一些必要的逻辑判断,有兴趣的可以去扒扒 Vue3 的源码研究研究。

4. 总结

本节主要介绍了 WeakMap 的使用和应用场景,这里要说明的一点是:WeakMap 不算真正意义上的弱引用方式,只要键仍然存在,它就强引用其上的内容。最新的 ES 方案提出了 WeakRef 的 API 作为真正的弱引用方式,现在还处于不稳定期间,也还存在一些问题,如果有兴趣的可以研究一下。最后,在 WeakMap 的使用上,大多数都是用来进行一些必要的逻辑判断的。在 WeakMap 实例上添加一个对已知对象的引用,从而在需要使用时,对该对象进行必要的逻辑判断。