weakMap、weakSet、weakRef、FinalizationRegistry傻傻分不清楚

583 阅读11分钟

一、从何说起

首先呢,这些内容都是与垃圾回收机制有关,通俗一点讲,就算我们不使用WeakMapweakSetweakRefFinalizationRegistry,JS的世界也是照常运转,它们在JS世界的地位,就和其名称一样——weak,弱。

属于属实有一些作用,但是成不了什么大事=。=类似于一些CSS里的功能选择器,虽然使用更简单更方便,但是平常使用JS实现的交互效果挺好的,所以并不那么流行与普及。

随着现在科技的发展、电脑硬件的更新换代、5G的普及,再加上我们平常开发的Web应用都是如此的简单,就算代码写的很拉,屎山上接着吃了很多内存那又如何,页面照样很流畅,用户一样是无感知。对于大多数的程序猿和应用程序来说,这些东西的商业价值是极低的。那是不是就没什么用,不用学了呢。

那当然不是了,否则写这篇文章做什么=。=

如果是超大型应用,或者用户基数庞大的产品,或者是服务器这种负载较高的场景,对于内存管理要求就很高,此时这些方法的优势就体现出来了。上述所有需要用到这些方法的场景一定是需要资深前端开发参与的场景,而如果你连这些方法及用法都不知道,就谈不上资深,挑上面的大梁可能会闪着腰。

也就是,你学会了这些以及类似的JS知识点,才有机会参与必须要使用这些JS特性的项目中,完成自我价值的证明与贡献。

所以,没有什么知识是没用的,只是时机的问题,所谓厚积薄发,就是这样一个意思。

二、垃圾?内存回收?

就内存管理这方面而言,JS开发人员可以归为下面几种:

  1. 内存?什么是内存?管理什么内存?我用js不就是因为它好用么,一个弱类型语言我想怎么写就怎么写,运行不挺好的!反正浏览器页面关掉什么都没了。
  2. 恩,这里这个对象之后没用了,我可以设置为null。意识是好的,设置也设置了,就是内存究竟有没有释放不得而知,看运气。
  3. 这里设置null不行,因为对象在其他地方也引用了,其他引用的地方也要删除,属于理解比较深刻的,JS基本功很扎实的。

举个大家比较容易懂的例子,已知页面中有个DOM元素,HTML结构如下:

<div id="dom">我只是个dom元素</div>

然后,需要删除此DOM元素,程序程序员么处理的:

var el = document.getElementById('dom');
el.remove();

这个处理有没有什么问题?

从效果上来看,完全解决了需求。

但是实际上,虽然页面中的元素删除了,但是内存中的这个DOM对象依然存在的。 我们可以这样测试一下

var el = document.getElementById('dom');
el.remove();

setTimeout(() => {
  document.body.append(el);
}, 1000);

就会看到div被删除了,然后过了1秒又出现在了页面上,因为el还在内存中,并未被清除掉,所以不会被回收。

所以,如果确定eleImage不再需要,需要多执行一句,同时设为 null。

var eleImage = document.getElementById('img');
eleImage.remove();
eleImage = null;

这样,JS在执行垃圾回收的时候,就会把el这个垃圾收回,释放内存。

这里就区分出来了上面所说的第一类Js开发人员和第二类开发人员

but!! 有时候,设置eleImage为null,并不能真正回收内存。

例如实际开发,有时候需要记住初始的outerHTML字符,方便还原。为了和原始DOM产生关联,有些开发就把DOM元素和outerHTML字符串放在同一个数组中进行管理:

var eleImage = document.getElementById('el');
var storage = {
    arrDom: [el, el.outerHTML]
};
eleImage.remove();
eleImage = null;

此时,el这个DOM对象其实还在内存中。因为 el 被 storage.arrDom 引用了,即使el设为null也无法将内存彻底释放。

测试下:

var eleImage = document.getElementById('el');
var storage = {
    arrDom: [el, el.outerHTML]
};
eleImage.remove();
eleImage = null;

setTimeout(() => {
  document.body.append(storage.arrDom[0]);
}, 1000);

同样可以看到,div被删除后,1秒后又出现了,神不神奇,气不气,那怎么办呢,别急 往下看

要想完全把div的内存释放,还需要执行下面这行:

storage.arrDom[0] = null;

如果在日常工作中可以做到这一步,那你在这一方面就可以归类为资深的JS前端开发那一类了。

但是,纯靠技术手段,人工识别哪些地方的内存要释放,实在是太累了。就算你会这部分知识,但是有时候嫌麻烦,都懒得去管理,那有没有什么方便点的方法,我只需要把变量设为null,所有引用的地方的内存都自动释放呢?

当然有!噔噔噔 主角们闪亮登场!!

三、WeakMap

定义

WeakMap是一种键值对的集合,类似于Map。不过,WeakMapMap有几个重要的区别:

  • WeakMap中,只有对象可以作为键。换句话说,我们不能使用基本类型(如数字,字符串,布尔值等)作为WeakMap的键。
  • WeakMap的键是弱引用的。这意味着,如果一个对象只被WeakMap引用,那么这个对象可以被垃圾回收(GC)。当这个对象被垃圾回收后,它对应的键值对也会从WeakMap中自动移除。
  • WeakMap不可遍历,也就是说,我们不能使用像for...of这样的循环来遍历WeakMap

由于这些特性,WeakMap在处理内存泄漏问题和管理对象私有数据等场景中有着显著的优势。

使用

上面的例子,如果使用WeakMap实现会是怎样的呢?

来吧 展示!

var eleImage = document.getElementById('el');
var storeMap = new WeakMap();
storeMap.set(el, el.outerHTML);
eleImage.remove();
eleImage = null;

同样是缓存div的outerHTML数据,但是这里使用了 WeakMap 对象,将 el 作为一个弱键,这样,el一旦设置为 null,所有相关的数据都会被释放。

当我们需要在某个对象上(ps:弱弱的问一下,大家应该都有且仅有一个对象吧)临时存放数据的时候,请使用WeakMap,尤其对于JS理解不是很深刻的开发人员,更是如此, 为什么呢?因为它省心啊,不要关心经常挂在嘴边的“内存泄露”问题。一个让人省心的对象简直让人美好

因为到时候只需要删除该对象,所有相关的引用和关联的内存都会被释放。

也就是,我不知道它内部怎么运转的,但是我这么写,哎你别说,你还真别说,性能也有了,逼格也高了!

语法

let myWm = new WeakMap()

此时,myWm就是一个新的WeakMap对象,包括了下面这些方法:

// 删除键
myWm.delete(key);
// 设置键和值
myWm.set(key, value);
// 是否包含某键
myWm.has(key);
// 获取键对应的值
myWm.get(key);

WeakMap和内存管理

WeakMap最重要的特性就是其键对对象的弱引用。这意味着,如果一个对象只被WeakMap引用,那么这个对象可以被垃圾回收。这样就可以防止因为长时间持有对象引用导致的内存泄漏。

例如,如果我们在Map中保存了一些对象的引用,即使这些对象在其他地方都已经不再使用,但是由于它们仍被Map引用,所以它们不能被垃圾回收,这就可能导致内存泄漏。然而,如果我们使用WeakMap来保存这些对象的引用,那么当这些对象在其他地方都不再使用时,它们就会被垃圾回收,从而防止了内存泄漏。

 WeakMap和对象私有数据

WeakMap还常常被用来保存对象的私有数据。这是因为WeakMap的键不可遍历,所以我们可以利用这个特性来存储一些只有特定代码能够访问的数据。

例如,我们可以创建一个WeakMap,然后使用这个WeakMap来保存每个对象的私有数据,像这样:

let privateData = new WeakMap();
function MyClass() {
    privateData.set(this, {secret: 'my secret data',});
}
MyClass.prototype.getSecret = function() {
    return privateData.get(this).secret;
};
let obj = new MyClass();
console.log(obj.getSecret()); // 输出: 'my secret data'

在这个例子中,我们创建了一个MyClass的类,每一个MyClass的实例都有一个私有数据secret。我们使用WeakMap来保存这个私有数据。这样,我们就可以在MyClass的方法中访问这个私有数据,但是其他的代码无法访问它。

四、weakSet

定义

WeakSet也是一种集合,类似于SetWeakSetSet的主要区别包括:

  • WeakSet中,只有对象可以作为值。也就是说,我们不能将基本类型(如数字,字符串,布尔值等)添加到WeakSet中。
  • WeakSet中的对象是弱引用的。如果一个对象只被WeakSet引用,那么这个对象可以被垃圾回收。当这个对象被垃圾回收后,它会自动从WeakSet中移除。
  • WeakSet不可遍历,也就是说,我们不能使用像for...of这样的循环来遍历WeakSet

WeakSet在处理对象的唯一性、内存泄漏等问题上有其独特的应用。

使用

我们可以使用new WeakSet()来创建一个新的WeakSet。在创建了WeakSet之后,我们可以使用add方法来添加新的对象,使用delete方法来移除某个对象,使用has方法来检查WeakSet中是否存在某个对象。

let weakSet = new WeakSet();
let obj1 = {};
let obj2 = {};// 添加对象
weakSet.add(obj1);
weakSet.add(obj2);// 检查对象是否存在
console.log(weakSet.has(obj1)); // 输出: true
console.log(weakSet.has(obj2)); // 输出: true// 删除对象
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // 输出: false

WeakSet和对象唯一性

WeakSet可以用来检查一个对象是否已经存在。由于WeakSet中的每个对象都是唯一的,所以我们可以利用这个特性来确保我们不会添加重复的对象。

例如,我们可以创建一个WeakSet,然后使用这个WeakSet来保存所有我们已经处理过的对象,像这样:

let processedObjects = new WeakSet();
function processObject(obj) {
    if (!processedObjects.has(obj)) {
        // 将对象添加到WeakSet中,表示我们已经处理过这个对象processedObjects.add(obj);
    }
}

在这个例子中,我们在每次处理一个对象之前,都会检查这个对象是否已经被处理过。如果这个对象已经被处理过,我们就不会再处理它。这样,我们就可以确保我们不会重复处理同一个对象。

WeakSet和内存管理

WeakMap一样,WeakSet中的对象也是弱引用的,所以WeakSet也有优秀的内存管理特性。如果一个对象只被WeakSet引用,那么这个对象可以被垃圾回收。这样就可以防止因为长时间持有对象引用导致的内存泄漏。

例如,如果我们在Set中保存了一些对象的引用,即使这些对象在其他地方都已经不再使用,但是由于它们仍被Set引用,所以它们不能被垃圾回收,这就可能导致内存泄漏。然而,如果我们使用WeakSet来保存这些对象的引用,那么当这些对象在其他地方都不再使用时,它们就会被垃圾回收,从而防止了内存泄漏。

五、weakRef

定义

weakRef是ES12新增的一种类,作用也是对对象进行弱引用。

使用

通过 new WeakRef() 创建一个 WeakRef 实例,传入要引用对象作为参数。

let p1 = {name: '张张'}
// 将 p1 赋值给 p2,是强引用
let p2 = p1

// 即使 p1 不再指向 {name: '张张'} 对象,但是 p2 仍然指向它,不会被回收
p1 = null 
let p1 = {name: '张张'}
// p2 对 {name: '张张'}  是弱引用
let p2 = new WeakRef(p1)

// p1 不再指向 {name: '张张'} 对象,某个时刻它就会被回收
p1 = null 

访问对象

如果想要访问创建出来的弱引用的对象,需要调用 deref() 方法来获取解引用后的对象。

const p1 = {name: '张张'}
const p2 = new WeakRef(p1)
// 如果此处将 p2.deref() 直接赋值给一个变量的话,将又会导致一次强引用
console.log(p2.deref()) //  {name: '张张'}

六、FinalizationRegistry

定义

FinalizationRegistry也是ES12新增的类,它的作用是用来监听某个对象是否被垃圾回收器回收。

使用

通过 new FinalizationRegistry() 创建一个 FinalizationRegistry 实例,可以传入一个回调函数作为参数,当某一个对象被回收时,传入的回调函数会被自动执行。

通过 实例.register() 来注册对象,可以注册多个对象。第一个参数就是要注册的对象;第二个参数可以在传入 new FinalizationRegister() 的回调函数中接收到,以此来区分被回收的是哪一个对象

let animal1 = {name: 'Tom'}
let animal2 = {name: 'Jerry'}

const finalizationRegistry = new FinalizationRegistry(type => {
    // 当某一个对象被回收时,会回调传入的函数,传入的函数自动执行
	console.log(`${type}对象被回收了`)
})

// 可以注册多个对象,第二个参数可以在传入 new FinalizationRegister() 的回调函数中接收到,以此来区分被回收的是哪一个对象
finalizationRegistry.register(animal1, 'cat')
finalizationRegistry.register(animal2, 'mouse')

p1 = null 
p2 = null