WeakMap的应用

71 阅读2分钟

什么是 WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合,但有两个区别。

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap的键名所指向的对象,不计入垃圾回收机制。

如何证明 WeakMap 不计入垃圾回收机制

我们无法直接查看引用是否存在,但我们可以通过观测内存使用来反应出是否存在引用。 nodejsprocess.memoryUsageglobal.gc刚好提供了手动gc与观测内存的功能,但在使用global.gc时要在启动node时添加额外的参数,具体命令如:$ node --expose-gc

测试代码

function logMemory(msg) {
  // 垃圾回收
  global.gc();
  console.log(process.memoryUsage().heapUsed / 10e5, 'M', msg);
}
logMemory('start');
let key = new Array(5 * 1024 * 1024);
logMemory('new Array');
let wm = new WeakMap();
logMemory('new WeakMap');

// 作为键名
// wm.set(key, 'information');
// logMemory('Array as WeakMap key');

// 作为键值
wm.set({}, key);
logMemory('Array as WeakMap value');

key = null;
logMemory('release Array');
wm = null;
logMemory('release WeakMap');

logMemory函数主要是手动gc和查看目前的内存使用情况。然后就是创建一个大数据量的Array,创建WeakMap,把Array当作WeakMap的键值或键名。然后消除Array的引用,消除WeakMap的引用。每个步骤直接都观测当前的内存状况。

代码执行结果如下:

作为键值

image.png

作为键名

image.png

结论

可以很明显看到,在release Array之后的内存情况,作为键名的内存一下就回到了3M左右,而作为键值的内存依然保持在45M,直到release WeakMap才回到3M。足以证明WeakMap的键名所指向的对象,不计入垃圾回收机制。

有什么用途呢?

给 DOM 节点添加信息并存储在 Map 结构中

根据前面所讲的特性,跟引用有关,跟对象有关,不难看出 WeakMap 的典型场合就是 DOM 节点作为键名。

给循环引用的对象进行深拷贝

编码

function deepClone(obj) {
  // 闭包缓存(使用WeakMap弱引用,记录拷贝缓存)
  let wm = new WeakMap();
  function _deepClone(obj) {
    // 是否引用数据类型
    if (typeof obj !== 'object') {
      return obj;
    }
    // 是否已拷贝,是否存在循环引用
    if (wm.has(obj)) return wm.get(obj);
    // 结果
    let res = {};
    // 缓存结果(这里必须在循环前,否则循环中可能出现递归,但还没又缓存,递归没有终止条件导致栈溢出)
    wm.set(obj, res);
    // 递归拷贝
    Object.keys(obj).forEach(key => {
      res[key] = _deepClone(obj[key]);
    })
    // 返回结果
    return res;
  }
  return _deepClone(obj)
}

测试

测试出无论是互相引用还是自身引用的对象都能完美克隆。


let obj1 = {
  b: 1,
};
let obj2 = {
  b: 2,
};
obj1.a = obj2;
obj2.a = obj1;
obj1.c = obj1;

let obj3 = deepClone(obj1);

// 以下为true
console.log(obj3.c === obj3);
console.log(obj3.a.a === obj3);

// 以下为false
console.log(obj3 === obj1);
console.log(obj3.c === obj1);
console.log(obj3.a.a === obj1);