明人不说暗话,五分钟带你搞懂 WeakMap

656 阅读3分钟

什么是 WeakMap ?

WeakMap 提供了一种从外部扩展对象而不干扰垃圾收集的方法。

它是一个 Map 字典,其中的键很弱,也就是说,如果对该键的所有引用都丢失,并且不再有对该值的引用,则可以对该值进行垃圾回收。

基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

需要注意的是,WeakMap 的 key 只能是 Object 类型。 原始数据类型是不能作为 key 的(比如 Symbol)。

举个栗子

假设有一个 API 给我们提供了一个特定的对象:

var obj = getObjectFromLibrary();

现在,我有一个使用该对象的方法:

function useObj(obj){
   doSomethingWith(obj);
}

我想跟踪该对象被调用的次数,如果调用次数超过 N,就进行上报处理。

大部分人很容易想到使用Map来进行实现:

var map = new Map(); 
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; 
    if(called > 10) report();
    map.set(obj, called);
}

这个方法是可行的,但它存在内存泄漏。

为什么使用 Map 会造成内存泄漏呢?下面我们一起来看一下。

为什么会造成内存泄漏?

在上面的例子中我们使用的是 Map,并使传入的对象作为映射键。

问题在于,这个对象永远不会从Map上被移除,因为我们不知道什么时候该去做这件事情。

所以总是有一个对它的引用,它永远不会被垃圾回收。

但是 在WeakMap 中,只要对象的所有其他引用都消失了,就可以从 WeakMap 中清除该对象。

WeakMap 示例

现在我们来看一下如何使用 WeakMap 解决这个问题:

var map = new WeakMap();
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; 
    if(called > 10) report(); 
    map.set(obj, called);
}

现在就不存在内存泄漏了。

一个真实的场景

WeakMap 可以用来从外部扩展对象,让我们再一起来看一个现实世界中和 Node.js 相关的例子。

假设你想要跟踪 Node.js 中所有当前被 rejected 的 Promises。

但是,你不想他们被垃圾回收,因为如果他们被垃圾回收了,这些 Promises 的引用就会丢失。

然而,如果保留对 Promises 的引用,则会导致内存泄漏,因为不会发生垃圾回收。

现在看来,无论如何你都需要保留每个被 rejected 的引用,但是我们可以使用 WeakMap ,既可以拿到每个 Promise 的引用,又可以使其被垃圾回收。

WeakMap 是怎么做到的?

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。

这意味着,我们无法对其进行枚举并且获得其 values。

但是 WeakMap 中,我们可以基于键存储数据,当该键被垃圾回收时,值也会被垃圾回收。

这意味着,你可以保持 Promise 的状态,并且该对象仍然可以被垃圾回收。

以后,如果你得到一个对象的引用,你可以检查你是否有任何与之相关的状态并报告它。

下面是Petka Antonov用来实现 未经处理的Promise钩子,如下所示:

process.on('unhandledRejection', function(reason, p) {
    console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
    // 进行其他的处理
});

我们在 WeakMap 中保存了相关 Promise 的信息,并且可以知道何时处理了被 rejected 的 promise。

使用场景

一些适合使用 WeakMap 防止内存泄漏的场景包括:

  • 保留关于特定对象的私有数据,并且只将对该对象的访问权限授予Map的引用者。
  • 保存有关对象的数据而不更改它们或产生开销。
  • 在浏览器中保存有关宿主对象(如DOM节点)的数据。
  • 从外部向对象添加功能。