(深入JavaScript 七)细讲:强引用与弱引用,Set、WeakSet、Map、WeakMap的使用与区别

208 阅读5分钟

强引用与弱引用

先说说引用是什么,一般来说强引用和弱引用针对的都是引用类型数据,而不是基本类型数据。引用类型数据就好比对象和数组,当然还有后面的集合(Set)和Map等等,它们都是需要在堆内存中新开辟一片空间进行存储的。比如,我们在定义一个对象的时候,如下面代码,这时候就存在引用了,变量obj对这个对象的引用:

let obj = {
    name:"aaa"
}

而强引用和弱引用有什么区别呢?又会带来怎样的影响?其实最主要的影响是对JS引擎的垃圾回收机制(GC)有关,当然这里我们说的是GC算法中的标记清除法。拿对象举例,当一个对象有强引用的关系存在时,GC就不会将其回收掉,如果一个对象没有任何引用或者是只存在弱引用,那么这个对象就会被GC给回收掉。

简而言之,强引用和弱引用的区别就在于,GC回收是不会管对象有没有弱引用存在的,只有对象存在强引用,才不会被回收掉。而对于弱引用是怎么来的需要等下我们讲到WeakSet和WeakMap的时候会遇到。

FinalizationRegistry

为了方便测试,我们介绍一个类:FinalizationRegistry。通过FinalizationRegistry创建出来的对象允许在上面注册一个对象,这个对象在被销毁的时候就会调用在new的时候传入的回调函数。注意,GC算法回收是不定时的,可能它几秒后就会打印,也可能是几十秒,所以使用该方法需要在控制台多等一下。

const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象被销毁", value);
});

let obj = { name: "aaa" };

finalRegistry.register(obj, "obj");

obj = null;

Set、WeakSet、Map、WeakMap的使用

Set

和数组类似,但区别在于Set内保存的元素不能重复,每个元素都是唯一的,创建时可以传入一个数组来创建。

Set常见的属性:

  • size:返回Set中元素的个数。

Set常用的方法:

  • add(value):添加某个元素,返回Set对象本身;
  • delete(value):从set中删除和这个值相等的元素,返回boolean类型;
  • has(value):判断set中是否存在某个元素,返回boolean类型;
  • clear():清空set中所有的元素,没有返回值;
  • forEach(callback, [, thisArg]):通过forEach遍历set;

另外,Set支持for of遍历

const arrSet = new Set([10,11])
arrSet.add(10)
arrSet.add(20)

arrSet.forEach(item => {
  console.log(item)
})

for (const item of arrSet) {
  console.log(item)
}

// delete
arrSet.delete(10)
console.log(arrSet)//Set(2) { 11, 20 }

// has
console.log(arrSet.has(11))// true

// clear
arrSet.clear()
console.log(arrSet)//Set(0) {}

WeakSet

WeakSet与Set基本一样,常用的属性和方法也和Set一致,但和Set的区别有两个:

  1. WeakSet中只能存放对象类型,不能存放基本数据类型
  2. WeakSet对元素的引用是弱引用

而上面我们也解释过,弱引用不会对GC回收对象产生影响,无论对象是否含有弱引用,只要它没有强引用就会被GC回收。下面我们结合FinalizationRegistry来测试一下Set和WeakSet的区别。

const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象被销毁", value);
});

let obj = { name: "aaa" };
let obj2 = { name: "bbb" };
finalRegistry.register(obj, "obj");
finalRegistry.register(obj2, "obj2");

const set1 = new Set([obj]);
const set2 = new WeakSet([obj2]);

obj = null;
obj2 = null;

image.png

上面代码中我们分别创建了Set和WeakSet并引用了obj和obj2,最后我们将这两个对象都赋值为null,也就是取消掉全局对这两个对象的强引用。等待一段时间后,我们发现我们定义的obj2被销毁了,但obj还存在,这就说明obj还存在引用,并且还是强引用,而这个引用来自set1对obj的引用。这也印证了WeakSet对对象是一个弱引用,上面代码虽然WeakSet引用了obj2,但obj2最后还是被销毁了。

Map

Map用于存储映射关系,它其实和对象有点像,对象也是用于存储映射关系的。但是在对象中,只能用字符串和Symbol作为属性名,而在某些情况下,我们可能需要将一个对象或者函数作为属性名,这时候就需要用到Map了。

Map常见的属性:

  • size:返回Map中元素的个数;

Map常见的方法:

  • set(key, value):在Map中添加key、value,并且返回整个Map对象;
  • get(key):根据key获取Map中的value;
  • has(key):判断是否包括某一个key,返回Boolean类型;
  • delete(key):根据key删除一个键值对,返回Boolean类型;
  • clear():清空所有的元素;
  • forEach(callback, [, thisArg]):通过forEach遍历Map;

Map也可以通过for of进行遍历。

const obj = { name: "aaa" };
const map = new Map([[obj, "aaa"]]);

// set
map.set("bbb", "111");
console.log(map);//Map(2) {{…} => 'aaa', 'bbb' => '111'}

//get
console.log(map.get(obj));//aaa

// has(key)
console.log(map.has(obj));//true

// delete(key)
map.delete(obj);
console.log(map);//Map(1) {'bbb' => '111'}

// clear
map.clear();
console.log(map);//Map(0) {size: 0}

const map2 = new Map([
  ["aaa", "111"],
  ["bbb", "222"],
]);

map2.forEach((item, key) => {
  console.log(item, key);
});

for (const item of map2) {
  console.log(item[0], item[1]);
}

for (const [key, value] of map2) {
  console.log(key, value);
}

WeakMap

WeakMap和Map基本一样,区别如下:

  1. WeakMap的key只能使用对象,不接受其他的类型作为key
  2. WeakMap的key对象的引用是弱引用

验证方法可参照WeakSet进行验证,对于WeakMap的应用,可在实现对象响应式中体现。详细可在我接下来准备写的vue2、vue3响应式原理这篇文章查看。

结尾

对于使用Set还是WeakSet,Map还是WeakMap,都取决于我们需不需要回收对象。因为含有强引用的对象是不能被GC回收的,为了避免内存泄漏,我们才会选择在合适情况下使用WeakSet和WeakMap来对一个对象进行弱引用,后续就算这个对象设置为null了,那么也不会存在被WeakSet和WeakMap的引用所束缚而不被GC回收,这样能很好的节省我们的内存。