JS WeakMap应该什么时候使用

这篇文章发布于 2021年08月15日,星期日,12:40,归类于 JS API。 阅读 22708 次, 今日 4 次 10 条评论

 

红梅葡萄占位图

一、从哪里开始呢?

算了,还是先说结论吧。

二、WeakMap什么时候用?

首先,大家要明白,就算没有 WeakMap,JS的世界也是照常运转,WeakMap在JS世界的地位,就和其名称一样——“weak”,弱。

属于那种有作用,但是并不在关键位置干大事的人,有点类似于一些CSS功能选择器,虽然使用更简单更方便,但是平常使用JS实现的交互效果挺好的,所以并不那么流行与普及。

WeakMap的作用就是可以更有效的垃圾回收、释放内存。

我们平常开发的Web应用都是如此的简单,就算代码很垃圾,吃了很多内存那又如何,页面照样流畅,用户照样无感知。

所以,对于绝大多数开发者和应用程序而言,WeakMap的商业价值是很低的。

这么一说,似乎WeakMap没什么好学的。

其实不然。

如果是超大型应用,或者用户基数庞大的产品,或者是服务器这种负载较高的场景,对于内存管理要求就很高,此时WeakMap的优势就可以体现。

这其实有点悖论的味道在里面。

上述所有需要用到WeakMap的场景都一定是需要资深前端开发参与的场景,而如果你连WeakMap都不知道,就谈不上资深,挑上面的大梁可能会闪着腰。

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

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

垃圾?内存?

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

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

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

<img id="img" src="https://bookcover.yuewen.com/qdbimg/349573/1027030348/180">

然后,需要删除此DOM元素,小明是这么处理的:

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

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

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

但是实际上,虽然页面中的图片元素删除了,但是内存中的这个DOM对象依然存在的。

我们不妨这样测试下:

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

setTimeout(() => {
  document.body.append(eleImage);
}, 2000);

就会看到图片被删除了(如下GIF录屏),然后过了2秒钟又出现在了页面上,因为eleImage还在内存中,并未清除,不会被回收。

删除后2秒又出现

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

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

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

大家可以看看自己是不关心内存的那类JS开发,还是会注意释放不需要的内存的JS开发。

OK,事情还没完,有时候,设置eleImage为null,并不能真正回收内存。

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

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

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

测试下:

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

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

同样可以看到,图片被删除后,2秒后又出现了。

这个还是看实时效果吧。

点击后面的按钮查看效果:

此时,要想完全把图像的内存释放,还需要执行下面这行:

storage.arrDom[0] = null;

大家试想下,如果是你,可以释放内存做到这一步吗?如果可以做到,那你就可以归类为资深的JS前端开发那一类了。

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

这就可以考虑使用WeakMap了。

使用场景

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

代码说话:

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

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

究竟内存释放没有,上面的例子不好测试,一是要借助工具,二是内存变化很小,看不出来。

更新于当日
经过评论反馈,FinalizationRegistry是可以检测垃圾回收的执行,并触发相应的回调的。

不过,我们可以使用Map对象对比下,如果是Map对象,eleImage作为键,那就是强引用,是一直在内存中的,例如:

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

setTimeout(() => {
  document.body.append(storeMap.keys().next().value);
}, 2000);

可以看到,虽然eleImage remove掉了,还设为了null,但是依然在内存中,可以append到页面中。

根据上面的分析和描述,我们可以得出下面的结论。

结论

当我们需要在某个对象上临时存放数据的时候,请使用WeakMap,尤其对于JS理解不是很深刻的开发人员,更是如此,因为省心,不要关心经常挂在嘴边的“内存泄露”问题。

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

也就是,虽然我看不懂代码是怎么执行的,但是我这么写的,性能就是好,逼格就是高!

看不懂

三、回到WeakMap语法本身

说了这么多,是时候介绍 WeakMap 语法的真身了。

语法

let myWm = new WeakMap()

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

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

说明

  1. key只能是对象,不能是原始数据类型(字符串、数字、true或false,null,undefined,symbol等类型)
  2. WeakMap中的键是无法枚举的

key只能是对象

下面的用法都是可以的:

myWm.set([], 1);
myWm.set(new Date(), '鑫空间');
myWm.set(()=>{}, 1);
myWm.set(document.createElement('by-zhangxinxu'), 1);

但是如果key不是对象,而是字符串之类的基本类型,就会报错,例如:

// 会报错
myWm.set('css新世界', true);

此时会报下面的错误:

“TypeError: Invalid value used as weak map key

因此WeakMap适合用在在对象上临时缓存数据的场景。

key无法枚举

不同于Map对象,Map对象是可以枚举的,有keys()values()entries()方法,还可以使用forEach遍历。

但是WeakMap无法枚举,WeakMap的这个特性也可以用来模拟私有属性。

const myWm = new WeakMap();
class Fish {
    constructor(name) {
        myWm.set(this, { 
            _fishbone: ['草鱼', '鲫鱼', '青鱼', '鲤鱼', '鲢鱼', '鳙鱼', '鳊鱼', '翘嘴', '餐条'],
        });
        this.name = name;
    }

    isBone() {
        return myWm.get(this)._fishbone.includes(this.name);
    }
}

// 测试,买了两条鱼
let fish1 = new Fish('草鱼');
let fish2 = new Fish('回鱼');

// 返回 true,有刺
console.log(fish1.isBone());
// 返回 false,没有肌间刺
console.log(fish2.isBone());

上面的代码中,_fishbone虽然和Fish对象相关联,但是却无法通过Fish对象直接获取。

如果不知道名称,也无法通过myWm遍历出来。

关于WeakMap更详细的语法介绍和示意可参考MDN文档:MDN WeakMap

四、结束语

考考大家,我昨天钓的下面3种鱼,哪种鱼有肌间刺,哪几种鱼没有肌间刺?

钓货

好,本文内容就上面这些。

如果文中有表述不准确的地方,或者有所遗漏,欢迎指正。

如果您觉得文章不错,也欢迎分享。

(本篇完)

分享到:


发表评论(目前10 条评论)

  1. kobe说道:

    nice!

  2. 手抓饼手抓饼说道:

    你好骚啊

  3. CodeHz说道:

    说实话用 WeakMap 来存 Element -> string 这种还是比较牵强的,毕竟你都拿到对象了,直接设置成属性不是也能达到一样的效果,怕名字撞了也可以用 Symbol
    我能想到的一个用法是在没有 private 属性的时候模拟 private 的效果(当然,这里说的是运行时强制性 private ,不是typescript的那种编译期检查)
    把想要加的私有属性放到 WeakMap 里(a.#f = x 变成 fmap.set(a, x)),那就只有能访问到那个 WeakMap 的对象能拿到“私有属性”的值,外部无法通过任何手段观测,即使使用 DevTools 也很难观察出这种“私有属性”,必须仔细检查源码才可以发现,相比于一般的 Map ,用 WeakMap 的好处自然就是它不需要关心原对象的内存释放问题

  4. AdblockUser说道:

    缝缝补补又三年,JS这东西破得很

  5. AdblockUser说道:

    只要 eleImage 所在的作用域结束, 变量不可达, 也是能正常回收的。
    顺便自定义大法好, 不 block js 也阔以 233

  6. adblockUser说道:

    just block js file can block your new ad….lol

  7. sanpang说道:

    厉害了,学习了!

  8. Lainbo说道:

    才在京东买的新世界,就发现了个adblock专享推荐,还是签名版的,哈哈哈哈哈

  9. jzz说道:

    WeakRef, FinalizationRegistry