在 ES6 引入的 Map 和 Set 之外,JavaScript 还提供了它们的“弱引用”版本:WeakMap 和 WeakSet。它们与 Map/Set 的核心区别在于对键(WeakMap)或值(WeakSet)持有的引用是弱引用,正是这一特性决定了它们独特的使用场景。
什么是弱引用
在 JavaScript 的垃圾回收机制中,一个对象只要存在至少一个强引用指向它,就不会被回收。而弱引用则完全不同:如果一个对象仅被弱引用指向,没有任何强引用,那么垃圾回收器可以随时回收它,即便它仍然存在于 WeakMap 或 WeakSet 中。
换句话说,WeakMap 和 WeakSet 不会阻止垃圾回收器回收它们所引用的对象。你往 WeakMap 里放了一个对象作为键,但在别处都不再使用这个对象时,它就自动“消失”了,对应的键值对也会被清除。
WeakMap 的特性与限制
WeakMap 的键必须是对象(不能是字符串、数字等原始类型),值可以是任意类型。这种设计从根本上是匹配弱引用语义的:如果允许原始类型作为键,弱引用就无法奏效,因为原始类型按值传递、没有引用计数的概念。
由于弱引用无法被迭代,WeakMap 不可遍历:它没有 size 属性,也不支持 keys()、values()、entries() 等迭代方法,更不能用 forEach。你只能通过 get(key)、set(key, value)、has(key)、delete(key) 操作已知的键。这种“不透明”的特性正是为了保证垃圾回收的确定性:如果允许遍历,垃圾回收器就无法判断哪些键还“有用”。
let obj = { name: 'temp' };
const wm = new WeakMap();
wm.set(obj, 'some metadata');
console.log(wm.get(obj)); // 'some metadata'
// 当 obj 失去所有强引用后
obj = null;
// 键值对会在下一次垃圾回收时自动清除,无法再被访问
WeakSet 的特性与限制
WeakSet 与 WeakMap 类似,它只能存放对象作为成员,不允许原始类型。同样地,WeakSet 不可遍历,没有 size 属性和遍历方法。你只能通过 add()、delete()、has() 操作。当一个对象在程序其他地方不再被持有,WeakSet 中对应的成员会被自动移除。
let user = { id: 1 };
const activeUsers = new WeakSet();
activeUsers.add(user);
console.log(activeUsers.has(user)); // true
user = null;
// 对象清除了,对应 WeakSet 成员也会消失
典型适用场景
WeakMap 和 WeakSet 是为元数据存储和对象标签而生的,当你需要将额外信息“贴”在某个对象上,但又不想因为这个“贴标签”行为阻止该对象被正常回收,它们就是最佳选择。
1. 存储私有数据或关联的元数据
在类和对象系统中,有时需要将一些内部状态与某个实例关联,但不方便作为实例本身的属性。WeakMap 可以轻松实现这一点:
const privateData = new WeakMap();
class User {
constructor(name, secret) {
privateData.set(this, { secret });
this.name = name;
}
getSecret() {
return privateData.get(this).secret;
}
}
const u = new User('Alice', 'my-password');
console.log(u.getSecret()); // 'my-password'
// 当 u 不再被使用时,WeakMap 中的关联数据也会被自动回收
这种方式相比将数据直接作为属性放在实例上,既保证了数据不被外部轻易访问,又不会影响垃圾回收。
2. 缓存计算结果(避免内存泄漏)
当你想对一个对象缓存某些计算结果,但不想因为这个缓存而阻止对象在不需要时被回收,WeakMap 就是理想载体。相比普通 Map 如果忘记手动清理会导致内存泄漏,WeakMap 会自动清理垃圾对象对应的缓存项。
const cache = new WeakMap();
function heavyCalculation(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = /* 昂贵的计算 */;
cache.set(obj, result);
return result;
}
3. 标记对象的状态(WeakSet)
WeakSet 适合用来给对象打标签,比如记录哪些对象正在被处理、已访问过等。因为 WeakSet 不会阻止对象被回收,所以用它来跟踪动态生命周期中的对象状态非常安全。
const visited = new WeakSet();
function process(node) {
if (visited.has(node)) return;
visited.add(node);
// ... 进行某种处理
}
在 DOM 操作中,也可以使用 WeakSet 来标记那些已经绑定了事件的 DOM 节点,避免重复绑定,同时不会因标记而阻止 DOM 节点被移除。
4. 防止内存泄漏的观察者模式
在实现发布-订阅模式或者事件监听时,如果订阅者是一个对象,直接将回调与对象关联存储在普通 Map 中,将导致对象无法释放。而使用 WeakMap 可以让订阅者在不再被其他地方引用时自动清理,避免长期订阅导致的内存泄漏。
选型判断:Map 还是 WeakMap?
- 如果你的键需要遍历(比如列出所有键),或者键本身是字符串/数字,只能用 Map。
- 如果你希望对象作为键,并且不想因为存储了这些键而阻止它们被垃圾回收,就应该用 WeakMap。
- 如果你需要知道集合的大小(size),也需要使用 Map 或 Set。
简单记忆原则:只要存储额外信息不想影响原有对象的生命周期,就用 Weak 版本。 这一机制在框架底层、图形编辑器、缓存系统和事件监听管理中被广泛使用,是编写内存友好型 JavaScript 应用的基础工具。