是什么
WeakMap在JS的地位,就和其名称一样“weak”,弱。
我们平常开发的Web应用都是相对简单,就算代码很垃圾,占用很多内存那又如何,页面照样流畅,用户照样无感知。
如果是超大型应用,或者用户基数庞大的产品,或者是服务器这种负载较高的场景,对于内存管理要求就很高,此时WeakMap的优势就可以体现。WeakMap的作用就是可以更有效的垃圾回收、释放内存。
如何用
内存管理而言,JS开发人员可以归为下面几种:
JS想怎么写就怎么写,运行不挺好的!反正浏览器页面关掉什么都没了。- 这个对象之后没用了,可以设置为
null。意思是好的,设置也设置了,就是内存究竟有没有释放不得而知。 - 这里设置
null不行,因为对象在其他地方也引用了,其他引用的地方也要删除。
举个比较容易理解的例子,已知页面中有个DOM元素,HTML结构如下:
<img id="img" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc9785782a8a40dc93f370684a6f95ed~tplv-k3u1fbpfcp-zoom-1.image">
需要删除此DOM元素,小明是这么处理的:
let eleImage = document.getElementById('img');
eleImage.remove();
从效果上来看,解决了需求。
但是实际上,虽然页面中的图片元素删除了,但是内存中的这个DOM对象依然存在的。
不妨这样测试下:
let eleImage = document.getElementById('img');
eleImage.remove();
setTimeout(() => {
document.body.append(eleImage);
}, 2000);
就会看到图片被删除了,然后过了2秒钟又出现在了页面上,因为eleImage还在内存中,并未清除,不会被回收。
所以,如果确定
eleImage不再需要,需要多执行一句,同时设为 null。
let eleImage = document.getElementById('img');
eleImage.remove();
eleImage = null;
这样,JS在执行垃圾回收的时候,就会把eleImage这个垃圾收回,释放内存。
但是,事情还没完!有时候,设置eleImage为null,并不能真正回收内存。
例如:实际开发中,有时候需要记住初始的outerHTML字符,方便还原。为了和原始DOM产生关联,就把DOM元素和outerHTML字符串放在同一个数组中进行管理。
let eleImage = document.getElementById('img');
let storage = {
arrDom: [eleImage, eleImage.outerHTML]
};
eleImage.remove();
eleImage = null;
此时,eleImage这个DOM对象其实还在内存中。因为eleImage被storage.arrDom引用了,即使eleImage设为null也无法将内存彻底释放。
let eleImage = document.getElementById('img');
let storage = {
arrDom: [eleImage, eleImage.outerHTML]
};
eleImage.remove();
eleImage = null;
setTimeout(() => {
document.body.append(storage.arrDom[0]);
}, 2000);
同样可以看到,图片被删除后,2秒后又出现了。
此时,要想完全把图像的内存释放,还需要执行下面这行:
storage.arrDom[0] = null;
但是,纯靠技术手段,人工识别哪些地方的内存要释放,实在是太累了。那有没有什么手段,我只要变量设为null,所有有引用的地方的内存都自动释放呢?
这就可以考虑使用WeakMap了。
上面的例子,如果使用WeakMap实现会是怎样的呢?
let eleImage = document.getElementById('img');
let storeMap = new WeakMap();
storeMap.set(eleImage, eleImage.outerHTML);
eleImage.remove();
eleImage = null;
同样是缓存图片的outerHTML数据,但是这里使用了WeakMap对象,将eleImage作为一个弱键,eleImage一旦设置为null,所有相关的数据都会被释放。
究竟内存释放没有,上面的例子不好测试,一是要借助工具,二是内存变化很小,看不出来。
不过,可以使用Map对象对比下,如果是Map对象,eleImage作为键,那就是强引用,是一直在内存中的,例如:
let eleImage = document.getElementById('img');
let 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。因为只需要删除该对象,所有相关的引用和关联的内存都会被释放。
巧用 WeakMap 实现深拷贝
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj
if (hash.get(obj)) return hash.get(obj)
let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) cloneObj[key] = deepClone(obj[key], hash)
}
return cloneObj
}
const obj1 = {
name: 'init',
arr: [1, [2, 3], 4],
};
const obj2 = deepClone(obj1)
obj2.name = "update";
obj2.arr[1] = [5, 6, 7];
console.log(obj1) // { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log(obj2) // { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
注意事项⚠️
key 只能是引用类型
myWm.set([], 1);
myWm.set(new Date(), '啦啦啦');
myWm.set(()=>{}, 1);
myWm.set(document.createElement('element'), 1);
如果key是基本类型,就会报错,例如:
// 会报错
myWm.set('css新世界', true); // 报错
因此WeakMap适合用在在对象上临时缓存数据的场景。
无法枚举
不同于Map对象,Map对象是可以枚举的,有keys()、values()、entries()方法,还可以使用forEach遍历。
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key); // F T
}
for (let value of map.values()) {
console.log(value); // no yes
}
for (let item of map.entries()) {
console.log(item[0], item[1]); // F no、T yes
}
for (let [key, value] of map.entries()) {
console.log(key, value); // F no、T yes
}
map.forEach(function(value, key, map) {
console.log(key, value); // F no、T yes
});
虽然WeakMap无法枚举,WeakMap的特性也可以用来模拟私有属性。
const myWm = new WeakMap();
class Animal {
constructor (name) {
myWm.set(this, {
_animal: ['熊猫', '大象', '长颈鹿', '鳄鱼'],
});
this.name = name;
}
isAnimal() {
return myWm.get(this)._animal.includes(this.name);
}
}
let animal1 = new Animal('熊猫');
let animal2 = new Animal('餐条');
console.log(animal1.isAnimal()); // true
console.log(animal2.isAnimal()); // false
参考资料:
JS WeakMap应该什么时候使用