无用知识集:JavaScript 中的 WeakMap

趁着清闲,在家学习 vue-next 源码,注意到其对 WeakMap应用;大家应该都知道,新版本 Vue 与旧版本相比,实现机制从 defineProperty 转变为 Proxy,却可能未曾注意到细致末节的差异。

ES6 之前,前端使用 Map 时,一般都是通过对象来模拟;对象的 key 值只能是字符串,即使传入的 key 值不是字符串形式,也会被转为字符串,而 Map 没有这种限制:

let o = {};
let o1 = {toString(){ return 1; }};
let o2 = {};
o[o1] = 1;
o[o2] = 2;
o; // {1: 1, [object Object]: 2}

WeakMap 与 Map 的区别

理论上的区别

  • WeakMap 的 key 只能是对象类型(null除了 typeof 的时候被当成对象的 bug,任何时候都不算对象):
const m1 = new Map();
const wm = new WeakMap();

const k1 = { foo: 1 };
const k2 = 'k2';

m1.set(k1, 'v1');
m1.set(k2, 'v2')
wm.set(k1, 'v1');
// wm.set(k2, 'v2'); // TypeError: Invalid value used as weak map key
  • WeakMap 的 key 不计入垃圾回收机制。WeakMap 的 key 所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内;一旦 key 不被其他地方引用,那么就会被回收。正是因为 WeakMap 键名的不确定性,它没有 keys()values()entries() 方法,也没有 size 属性;此外,WeakMap 还没有 clear() 方法(在最初的时候,是有这个方法的)。

实操

前面说了,WeakMap 相对于 Map 的一个重要区别就是其垃圾回收机制;空口无凭,下面用一段代码来展示其回收效果(因为浏览器环境下,无法通过代码控制垃圾回收,所以以下代码通过 Node.js 运行,node --expose-gc 来开启手动清理):

// 打印方法格式化
function format(value) {
    return `${(value / 1024 / 1024).toFixed(2)} M`
}
function print(m) {
    console.log(`HeapTotal: ${format(m.heapTotal)}`);
    console.log(`HeapUsed: ${format(m.heapUsed)}`);
}

// 手动垃圾回收
global.gc();
console.log('初始化:');
print(process.memoryUsage()); // node中查看内存状态的方法,我们目前只需要关注 heapTotal,heapUsed
let vm = new WeakMap();
let key = new Array(20 * 1024 * 1024);
vm.set(key, 'foo');
global.gc();
console.log('回收前:');
print(process.memoryUsage());

key = null; // key 置为 null 之后,只被 vm 键名引用
global.gc();
console.log('回收后:');
print(process.memoryUsage());
// 上面代码的运行结果:
// node --expose-gc weakmap-test.js
// 初始化:
// HeapTotal: 4.52 M
// HeapUsed: 1.86 M
// 回收前:
// HeapTotal: 166.52 M
// HeapUsed: 161.71 M
// 回收后:
// HeapTotal: 10.52 M
// HeapUsed: 1.71 M

如果将上述代码中的 WeakMap 替换为 Map 的话,运行结果为:

初始化:
HeapTotal: 4.52 M
HeapUsed: 1.86 M
回收前:
HeapTotal: 166.52 M
HeapUsed: 161.71 M
回收后:
HeapTotal: 170.52 M
HeapUsed: 161.71 M

使用 Map 时,由于数组占用的内存未被回收,HeapUsed 在垃圾回收前后无差异;而使用 WeakMap 时,在垃圾回收后,内存恢复。另外,我们在代码中创建了一个长度为 20M 的空数组,但其占用的内存却约为 160M,也就是说,每个空元素占用的内存大小为 8 个字节。如果有兴趣的话,可以查看在 Chrome 中 JavaScript 数组到底占用了多少内存?

在浏览器中,可以通过开发者工具来查看使用 WeakMap 和 Map 时对内存的影响:

const map = new WeakMap(); // 将 WeakMap 替换为 Map 后,再次点击 Take Heap Snapshot,耗时明显增加;并且从图中可以看出,array 占用的内存,并没有被回收。
(function () {
  // 立即执行函数,执行完之后,除了 map/weakmap 之外,无其他方式可以访问到;当其被作为 map 的 key 时,内存不会回收;而作为 WeakMap 的 key 时,执行完成后会被回收;
  const arr1 = new Array(20 * 1024 * 1024);
  const arr2 = new Array(20 * 1024 * 1024);
  const arr3 = new Array(20 * 1024 * 1024);
  const arr4 = new Array(20 * 1024 * 1024);
  const arr5 = new Array(20 * 1024 * 1024);
  const arr6 = new Array(20 * 1024 * 1024);
  const arr7 = new Array(20 * 1024 * 1024);
  const arr8 = new Array(20 * 1024 * 1024);
  const arr9 = new Array(20 * 1024 * 1024);
  const arr10 = new Array(20 * 1024 * 1024);
  const arr11 = new Array(20 * 1024 * 1024);
  const arr12 = new Array(20 * 1024 * 1024);
  const arr13 = new Array(20 * 1024 * 1024);
  const arr14 = new Array(20 * 1024 * 1024);
  const arr15 = new Array(20 * 1024 * 1024);
  const arr16 = new Array(20 * 1024 * 1024);
  const arr17 = new Array(20 * 1024 * 1024);
  const arr18 = new Array(20 * 1024 * 1024);
  map.set(arr1, 'arr1');
  map.set(arr2, 'arr2');
  map.set(arr3, 'arr3');
  map.set(arr4, 'arr4');
  map.set(arr5, 'arr5');
  map.set(arr6, 'arr6');
  map.set(arr7, 'arr7');
  map.set(arr8, 'arr8');
  map.set(arr9, 'arr9');
  map.set(arr10, 'arr10');
  map.set(arr11, 'arr11');
  map.set(arr12, 'arr12');
  map.set(arr13, 'arr13');
  map.set(arr14, 'arr14');
  map.set(arr15, 'arr15');
  map.set(arr16, 'arr16');
  map.set(arr17, 'arr17');
  map.set(arr18, 'arr18');
})();

WeakMap 执行结果:
无用知识集:JavaScript 中的 WeakMap_第1张图片

在前端项目中对 WeakMap 的应用场景,可以参见阮一峰老师 WeakMap-的用途

参考:

你可能感兴趣的:(javascript,前端,node.js,vue.js)