面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

34 WeakMaps (WeakMap)(进阶)



WeakMaps 类似于 Maps,但有以下区别:

接下来的两节 将更详细地研究这意味着什么。

34.1 WeakMaps 是黑盒

无法检查 WeakMap 中的内容。

这些限制实现了一个安全属性。引用 Mark Miller 的话:

只有同时拥有 weakmap 和键的人才能观察或影响 weakmap/键对值的映射。使用 clear(),只有 WeakMap 的人就可以影响 WeakMap 和键到值的映射。

34.2 WeakMap 的键是被弱引用的

WeakMap 的键被称为是被弱引用的:通常,如果一个对象引用了另一个对象,那么只要前者存在,后者就不能被垃圾回收。对于 WeakMap,情况就不同了:如果一个对象是一个键,并且没有被其他地方引用,那么即使 WeakMap 仍然存在,它也可能被垃圾回收。这也导致相应的条目被删除(但无法观察到这一点)。

34.2.1 所有 WeakMap 键必须是对象

所有 WeakMap 键必须是对象。如果您使用原始值,则会出错:

> const wm = new WeakMap();
> wm.set(123, 'test')
TypeError: Invalid value used as weak map key

如果将原始值作为键,WeakMaps 将不再是黑盒。但是,鉴于原始值永远不会被垃圾回收,因此您无论如何都不会从弱引用的键中获益,并且可以像使用普通 Map 一样使用它。

34.2.2 用例:将值附加到对象

这是 WeakMaps 的主要用例:您可以使用它们将值从外部附加到对象——例如:

const wm = new WeakMap();
{
  const obj = {};
  wm.set(obj, 'attachedValue'); // (A)
}
// (B)

在 A 行,我们将一个值附加到 obj。在 B 行,即使 wm 仍然存在,obj 也可能已经被垃圾回收。这种将值附加到对象的技巧等效于将该对象的属性存储在外部。如果 wm 是一个属性,则前面的代码将如下所示:

{
  const obj = {};
  obj.wm = 'attachedValue';
}

34.3 示例

34.3.1 通过 WeakMaps 缓存计算结果

使用 WeakMaps,您可以将先前计算的结果与对象相关联,而不必担心内存管理。以下函数 countOwnKeys() 是一个示例:它将先前结果缓存在 WeakMap cache 中。

const cache = new WeakMap();
function countOwnKeys(obj) {
  if (cache.has(obj)) {
    return [cache.get(obj), 'cached'];
  } else {
    const count = Object.keys(obj).length;
    cache.set(obj, count);
    return [count, 'computed'];
  }
}

如果我们将此函数与对象 obj 一起使用,您可以看到结果仅针对第一次调用计算,而第二次调用使用缓存的值:

> const obj = { foo: 1, bar: 2};
> countOwnKeys(obj)
[2, 'computed']
> countOwnKeys(obj)
[2, 'cached']

34.3.2 在 WeakMaps 中保存私有数据

在以下代码中,WeakMaps _counter_action 用于存储 Countdown 实例的虚拟属性的值:

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

// The two pseudo-properties are truly private:
assert.deepEqual(
  Object.keys(new Countdown()),
  []);

以下是 Countdown 的使用方法:

let invoked = false;

const cd = new Countdown(3, () => invoked = true);

cd.dec(); assert.equal(invoked, false);
cd.dec(); assert.equal(invoked, false);
cd.dec(); assert.equal(invoked, true);

  练习:用于私有数据的 WeakMaps

exercises/weakmaps/weakmaps_private_data_test.mjs

34.4 WeakMap API

WeakMap 的构造函数和四个方法的工作方式与 它们的 Map 等效项 相同:

  测验

请参阅 测验应用程序