WeakMap

弱映射(WeakMap)是一种新的集合引用类型,

基本 API

可以使用 new 关键字实例化一个空的 WeakMap:

const wm = new WeakMap();

弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出 TypeError

new WeakMap(465);
// TypeError: number 465 is not iterable (cannot read property Symbol(Symbol.iterator))

值的类型没有限制。

如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键值对数组。可迭代对象中的每个键值对都会按照迭代顺序插入新实例中:

const key1 = { id: 1 },
  key2 = { id: 2 },
  key3 = { id: 3 };

// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([
  [key1, "val1"],
  [key2, "val2"],
  [key3, "val3"],
]);

console.log(wm1.get(key1)); // val1
console.log(wm1.get(key2)); // val2
console.log(wm1.get(key3)); // val3

// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
  [key1, "val1"],
  ["BADKEY", "val2"],
  [key3, "val3"],
]);
// Uncaught TypeError: Invalid value used as weak map key

// 原始值可以先包装成对象再用作键
const stringKey = new String("key1");
const wm3 = new WeakMap([stringKey, "val1"]);
console.log(wm3.get(stringKey)); // "val1"

初始化之后可以使用 Set() 再添加键值对,可以使用 get()has() 查询,还可以使用 delete() 删除:

const wm = new WeakMap();
const key1 = { id: 1 },
  key2 = { id: 2 };
console.log(wm.has(key1)); // false
console.log(wm.get(key1)); // undefined

wm.set(key1, "Matt").set(key2, "Frisbie");

console.log(wm.has(key1)); // true
console.log(wm.get(key1)); // Matt

wm.delete(key1); // 只删除这一个键值对

console.log(wm.has(key1)); // false
console.log(wm.get(key1)); // true

set() 方法返回若映射实例,因此可以把多个操作连缀起来,包括初始化声明:

const key1 = { id: 1 },
  key2 = { id: 2 },
  key3 = { id: 3 };

const wm = new WeakMap().set(key1, "val1");

wm.set(key2, "val2").set(key3, "val3");

console.log(wm.get(key1)); // val1
console.log(wm.get(key2)); // val2
console.log(wm.get(key3)); // val3

弱键

WeakMap 中 “weak” 表示弱映射的键是 “弱弱地拿着” 的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是 “弱弱地拿着” 的。只要键存在,键值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。

const wm = new WeakMap();
wm.set({}, "val");

set() 方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键值对就从弱映射中消失了,使其称为一个空映射。在这个例子中,因为值也没有被引用,所以这个键值对被破坏后,值本身也会成为垃圾回收的目标。

再看一个稍微不同的例子:

const wm = new WeakMap();

const container = {
  key: {},
};

wm.set(container.key, "val");

function removeRefercence() {
  container.key = null;
}

这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标,不过,如果调用 removeRefercence(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键值对清理掉。

不可迭代键

因为 WeakMap 中的键值对任何时候都可能被销毁,所以没必要提供迭代其键值对的能力。当然,也用不着像 clear() 这样一次性销毁所有键值对的方法。WeakMap 确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问 WeakMap 实例,也办法看到其中的内容。

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。

使用弱映射

因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。

const m = new Map();
const loginButton = document.querySelector("#login");
// 给这个节点关联一些元数据
m.set(loginButton, { disabled: true });

假设在上面的代码执行后,页面被 JavaScript 改变了,原来的登录按钮从 DOM 树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。

如果这里使用的是弱映射,如以下代码所示,那么节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存。

const wm = new WeakMap();
const loginButton = document.querySelector("#login");
wm.set(loginButton, { disabled: true });