ES6 系列之 WeakMap

发布于 2022-07-15 08:50:40 字数 5076 浏览 273 评论 17

我们先从 WeakMap 的特性说起,然后聊聊 WeakMap 的一些应用场景。

特性

1. WeakMap 只接受对象作为键名

const map = new WeakMap();
map.set(1, 2);
// TypeError: Invalid value used as weak map key
map.set(null, 2);
// TypeError: Invalid value used as weak map key

2. WeakMap 的键名所引用的对象是弱引用

这句话其实让我非常费解,我个人觉得这句话真正想表达的意思应该是:

WeakMaps hold "weak" references to key objects,

翻译过来应该是 WeakMaps 保持了对键名所引用的对象的弱引用。

我们先聊聊弱引用:

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

在 JavaScript 中,一般我们创建一个对象,都是建立一个强引用:

var obj = new Object();

只有当我们手动设置 obj = null 的时候,才有可能回收 obj 所引用的对象。

而如果我们能创建一个弱引用的对象:

// 假设可以这样创建一个
var obj = new WeakObject();

我们什么都不用做,只用静静的等待垃圾回收机制执行,obj 所引用的对象就会被回收。

我们再来看看这句:WeakMaps 保持了对键名所引用的对象的弱引用

正常情况下,我们举个例子:

const key = new Array(5 * 1024 * 1024);
const arr = [
  [key, 1]
];

使用这种方式,我们其实建立了 arr 对 key 所引用的对象(我们假设这个真正的对象叫 Obj)的强引用。所以当你设置 key = null 时,只是去掉了 key 对 Obj 的强引用,并没有去除 arr 对 Obj 的强引用,所以 Obj 还是不会被回收掉。

Map 类型也是类似:

let map = new Map();
let key = new Array(5 * 1024 * 1024);

// 建立了 map 对 key 所引用对象的强引用
map.set(key, 1);
// key = null 不会导致 key 的原引用对象被回收
key = null;

我们可以通过 Node 来证明一下这个问题:

// 允许手动执行垃圾回收机制
node --expose-gc

global.gc();
// 返回 Nodejs 的内存占用情况,单位是 bytes
process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M

key = null;
global.gc();
process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M

// 这句话其实是无用的,因为 key 已经是 null 了
map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M

如果你想要让 Obj 被回收掉,你需要先 delete(key) 然后再 key = null:

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
map.delete(key);
key = null;

我们依然通过 Node 证明一下:

node --expose-gc

global.gc();
process.memoryUsage(); // heapUsed: 4638376 ≈ 4.4M

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46727816 ≈ 44.6M

map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46748352 ≈ 44.6M

key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4808064 ≈ 4.6M

这个时候就要说到 WeakMap 了:

const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
key = null;

当我们设置 wm.set(key, 1) 时,其实建立了 wm 对 key 所引用的对象的弱引用,但因为 let key = new Array(5 * 1024 * 1024) 建立了 key 对所引用对象的强引用,被引用的对象并不会被回收,但是当我们设置 key = null 的时候,就只有 wm 对所引用对象的弱引用,下次垃圾回收机制执行的时候,该引用对象就会被回收掉。

我们用 Node 证明一下:

node --expose-gc

global.gc();
process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M

const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M

key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M

所以 WeakMap 可以帮你省掉手动删除对象关联数据的步骤,所以当你不能或者不想控制关联数据的生命周期时就可以考虑使用 WeakMap。

总结这个弱引用的特性,就是 WeakMaps 保持了对键名所引用的对象的弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

也正是因为这样的特性,WeakMap 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakMap 不可遍历。

所以 WeakMap 不像 Map,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有 size 属性,也不支持 clear 方法,所以 WeakMap只有四个方法可用:get()、set()、has()、delete()。

应用

1. 在 DOM 对象上保存相关数据

传统使用 jQuery 的时候,我们会通过 $.data() 方法在 DOM 对象上储存相关信息(就比如在删除按钮元素上储存帖子的 ID 信息),jQuery 内部会使用一个对象管理 DOM 和对应的数据,当你将 DOM 元素删除,DOM 对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData() 方法才能删除掉相关联的数据,WeakMap 就可以简化这一操作:

let wm = new WeakMap(), element = document.querySelector(".element");
wm.set(element, "data");

let value = wm.get(elemet);
console.log(value); // data

element.parentNode.removeChild(element);
element = null;

2. 数据缓存

从上一个例子,我们也可以看出,当我们需要关联对象和数据,比如在不修改原有对象的情况下储存某些属性或者根据对象储存一些计算的值等,而又不想管理这些数据的死活时非常适合考虑使用 WeakMap。数据缓存就是一个非常好的例子:

const cache = new WeakMap();
function countOwnKeys(obj) {
    if (cache.has(obj)) {
        console.log('Cached');
        return cache.get(obj);
    } else {
        console.log('Computed');
        const count = Object.keys(obj).length;
        cache.set(obj, count);
        return count;
    }
}

3. 私有属性

WeakMap 也可以被用于实现私有变量,不过在 ES6 中实现私有变量的方式有很多种,这只是其中一种:

const privateData = new WeakMap();

class Person {
    constructor(name, age) {
        privateData.set(this, { name: name, age: age });
    }

    getName() {
        return privateData.get(this).name;
    }

    getAge() {
        return privateData.get(this).age;
    }
}

export default Person;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(17

捂风挽笑 2022-05-04 13:49:52

Node.js版本v14.15.1

process.memoryUsage(); // heapUsed: 3MB左右
const vm = new WeakMap();

let key = new Array(5 * 1024 * 1024);
process.memoryUsage(); // heapUsed: 44MB左右
vm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 44MB左右

// 这里把key设置为null了之后,我在测了一下memory
key = null;
process.memoryUsage(); // heapUsed: 88MB左右

global.gc();
process.memoryUsage(); // heapUsed: 3MB左右

@mqyqingfeng 大佬,为啥我调用key = null后, 内存奇怪的使用更多了是什么原因。

韬韬不绝 2022-05-04 13:49:52

var t;
const privateData = new WeakMap();
class Person {
constructor(name, age) {
t=this;
console.log(this);
privateData.set(t, { name: name, age: age });
}

getName() {
    return privateData.get(this).name;
}

getAge() {
    return privateData.get(this).age;
}

}

let a=new Person('andy',18)
console.log(a,privateData,t===a,privateData.get(a));
t=a=null;
setInterval(v=>{
console.log(privateData);
},600)

t=a=null; 发现还是去除不了

我测了下是可以的,一些建议

  1. 先用node命令行
  2. 将name和age改成长数组,可以明显的看到内存变化。
说不完的你爱 2022-05-04 13:49:52

WeakMap 中四个方法好实现么
一般我们讲JS实现,其实是重复造个语法糖(async await)、高级对象(Set)、高级函数(Promise.all)的轮子。
WeakMap不做实现的原因主要是Weak特性不好实现,因为弱引用就需要用弱引用来实现。
https://www.infoq.cn/article/lksmb2tlgh1ehg0*bbyg 这篇文章看到有用WeakRef实现WeakMap,这样的底层相互实现就没有必要了。

近箐 2022-05-04 13:49:51

以前看阮一峰老师的ES6教程硬是没看懂,不知道是自己变强了还是老师写得太好了,茅塞顿开!嘿嘿

经典没看懂

む无字情书 2022-05-04 13:49:51

终于明白了弱引用,感谢

乞讨 2022-05-04 13:49:51

垃圾回收机制回收了弱引用,那么相当于删除了那个引用的数据吗?为什么执行代码一段时间后再输出 wm,设置的 wm.set([3,4], 6) 这一项数据为什么也没有了?wm 变成空的了,代码是在浏览器中执行的

狼亦尘 2022-05-04 13:49:51

垃圾回收机制回收了弱引用,那么相当于删除了那个引用的数据吗?为什么执行代码一段时间后再输出 wm,设置的 wm.set([3,4], 6) 这一项数据为什么也没有了?wm 变成空的了,代码是在浏览器中执行的

因为vm.set([3,4],6) 中的[3,4] 没有其他地方引用,垃圾回收时会直接回收,而
let key = [3,5];
vm.set(key,1);
[3,5]被key引用,把key置为null之后[3,5]才会被回收

溺深海 2022-05-04 13:49:50

应该这么翻译:WeakMap 对键名的引用属于弱引用
不必强调”保持“

小嗷兮 2022-05-04 13:49:49

是不可以简单理解,weakmap系统可以自动清除,map需要手动

把昨日还给我 2022-05-04 13:49:44

冴羽老师有一点我不太明白,阮一峰老师在ES6当中提到WeakSet以及WeakMap解决了 "引用计数" 这种垃圾回收机制存在的问题,但是在JavaScript高级程序设计一书当中表示现在的各大浏览器的垃圾回收机制已经全部使用的是 "标记清除" ,既然浏览器都已经替我们解决了这个问题,使用WeakSet和WeakMap还有什么作用吗?还请老师指点一二

无敌元气妹 2022-05-04 13:49:41

emm,小白表示基础差看不大明白,知其然不知其所以然。希望不就之后再来会有茅厕顿开的感觉,先给老师32个赞

感性 2022-05-04 13:46:42

var t;
const privateData = new WeakMap();
class Person {
constructor(name, age) {
t=this;
console.log(this);
privateData.set(t, { name: name, age: age });
}

getName() {
    return privateData.get(this).name;
}

getAge() {
    return privateData.get(this).age;
}

}

let a=new Person('andy',18)
console.log(a,privateData,t===a,privateData.get(a));
t=a=null;
setInterval(v=>{
console.log(privateData);
},600)

t=a=null; 发现还是去除不了

自由如风 2022-05-04 13:09:31

例子举的太好了!

〗斷ホ乔殘χμё〖 2022-05-04 12:09:29

WeakMap 中四个方法好实现么

被你宠の有点坏 2022-05-04 09:59:09

以前看阮一峰老师的ES6教程硬是没看懂,不知道是自己变强了还是老师写得太好了,茅塞顿开!嘿嘿

猫烠⑼条掵仅有一顆心 2022-05-03 23:56:39

感谢冴羽老师的分享

暗藏城府 2022-05-03 17:08:25

感谢,写的很清晰明了

~没有更多了~

关于作者

笨死的猪

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文