巧用 WeakMap, 释放内存更便捷

1,782 阅读4分钟

是什么

WeakMapJS的地位,就和其名称一样“weak”,弱。
我们平常开发的Web应用都是相对简单,就算代码很垃圾,占用很多内存那又如何,页面照样流畅,用户照样无感知。
如果是超大型应用,或者用户基数庞大的产品,或者是服务器这种负载较高的场景,对于内存管理要求就很高,此时WeakMap的优势就可以体现。WeakMap的作用就是可以更有效的垃圾回收、释放内存。

如何用

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

  1. JS想怎么写就怎么写,运行不挺好的!反正浏览器页面关掉什么都没了。
  2. 这个对象之后没用了,可以设置为null。意思是好的,设置也设置了,就是内存究竟有没有释放不得而知。
  3. 这里设置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这个垃圾收回,释放内存。
但是,事情还没完!有时候,设置eleImagenull,并不能真正回收内存。

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

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

此时,eleImage这个DOM对象其实还在内存中。因为eleImagestorage.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应该什么时候使用