WeakMap 是 JavaScript 中一种非常有用的数据结构,它通过弱引用机制来帮助管理内存,防止内存泄漏。简单来说,当你用一个对象作为 WeakMap 的键时,WeakMap 不会阻止这个对象被垃圾回收器回收。一旦这个对象在其他地方没有被引用了,它以及它在 WeakMap 中对应的值就会被自动清理掉。
下面是一个对比 WeakMap 和 Map 主要特性的表格,帮助你快速了解它们的区别:
特性 | WeakMap | Map |
---|---|---|
键类型 | 只接受对象作为键 | 任何类型(对象、原始值)均可作为键 |
引用机制 | 对键是弱引用,不阻止垃圾回收 | 对键是强引用,防止键被垃圾回收 |
可遍历性 | 不可遍历 (无 keys() , values() , entries() 方法,无 size 属性) |
可遍历 ,有 size 属性 |
内存管理 | 自动清理,不易内存泄漏 | 需手动管理,可能内存泄漏 |
主要使用场景 | 需要与对象生命周期关联的元数据、缓存或私有数据存储 | 需要频繁遍历、查询或维护固定键值对集合的场景 |
WeakMap 主要应用场景与 Demo
WeakMap 的设计特点使得它特别适合用于那些需要将数据与对象关联,但又不想影响这些对象生命周期(即垃圾回收)的场景。
1. 为 DOM 元素存储元数据
当需要为 DOM 元素添加一些附加数据(如状态、事件处理器等)时,如果直接存储在普通对象或 Map 中,即使 DOM 元素从页面上移除,由于 Map 还引用着它,它也不会被垃圾回收,导致内存泄漏。WeakMap 可以自动解决这个问题。
javascript
// 创建一个 WeakMap 来存储每个 div 元素的点击次数
const domElementMetadata = new WeakMap();
// 获取一个 div 元素
const myDiv = document.createElement('div');
document.body.appendChild(myDiv);
// 为该 div 元素初始化元数据
domElementMetadata.set(myDiv, { clickCount: 0 });
// 给 div 添加点击事件,更新元数据
myDiv.addEventListener('click', function() {
const metadata = domElementMetadata.get(myDiv);
metadata.clickCount++;
console.log(`该 div 已被点击 ${metadata.clickCount} 次`);
});
// 假设未来某个时刻,myDiv 从 DOM 中被移除,并且没有其他变量引用它
// myDiv.remove(); // 从 DOM 移除
// myDiv = null; // 移除引用
// 此后,垃圾回收器可以自动回收 myDiv 对象,domElementMetadata 中对应的键值对也会被自动清除
2. 存储对象的私有数据
在 JavaScript 中,实现真正的私有成员比较麻烦。WeakMap 可以用于模拟对象的私有属性,这些私有属性会随着对象的销毁而自动消失。
javascript
// 使用 WeakMap 来模拟私有属性
const _privateData = new WeakMap();
class MyClass {
constructor(publicValue) {
// 将私有数据存储在 WeakMap 中,以当前实例 this 为键
_privateData.set(this, {
secret: `This is a secret for ${publicValue}`,
internalCounter: 0
});
this.publicValue = publicValue;
}
getSecret() {
// 只有通过实例方法才能访问到对应的私有数据
const data = _privateData.get(this);
data.internalCounter++;
return data.secret;
}
getCallCount() {
return _privateData.get(this).internalCounter;
}
}
// 使用类
const instance1 = new MyClass('Instance One');
console.log(instance1.getSecret()); // "This is a secret for Instance One"
console.log(instance1.getCallCount()); // 1
const instance2 = new MyClass('Instance Two');
console.log(instance2.getSecret()); // "This is a secret for Instance Two"
console.log(instance2.getCallCount()); // 1
// 当 instance1 被置为 null,它就可以被垃圾回收,_privateData 中对应的私有数据也会被自动清理
// instance1 = null;
3. 缓存计算结果
当需要根据特定对象缓存耗时的计算结果,并且希望缓存的生命周期与该对象保持一致时,WeakMap 是很好的选择。
javascript
// 使用 WeakMap 缓存与对象相关的昂贵计算结果
const computationCache = new WeakMap();
function intensiveComputation(obj) {
// 如果缓存中存在该对象的结果,则直接返回
if (computationCache.has(obj)) {
console.log('从缓存中获取结果');
return computationCache.get(obj);
}
// 模拟一个耗时的计算过程
console.log('执行计算...');
const result = JSON.stringify(obj); // 假设这是一个昂贵的操作
// 将计算结果缓存到 WeakMap 中,以输入对象为键
computationCache.set(obj, result);
return result;
}
// 使用缓存函数
const inputObj1 = { data: "test1" };
const result1 = intensiveComputation(inputObj1); // 输出 "执行计算..."
const result1Cached = intensiveComputation(inputObj1); // 输出 "从缓存中获取结果"
const inputObj2 = { data: "test2" };
const result2 = intensiveComputation(inputObj2); // 输出 "执行计算..."
// 当 inputObj1 不再被需要,并被置为 null 时
// inputObj1 = null;
// 垃圾回收后,computationCache 中对应的缓存项也会被自动清除
⚠️ 使用 WeakMap 的注意点
- 键必须是对象 :WeakMap 的键只能是对象 (包括数组、函数等),不能是原始值(如字符串、数字、Symbol、
null
、undefined
)。尝试使用原始值作为键会抛出TypeError
。 - 不可遍历 :由于弱引用的特性,WeakMap 没有
size
属性,也不能 遍历其键或值(例如,没有keys()
,values()
,entries()
方法,也不能使用forEach
)。你只能通过get(key)
,set(key, value)
,has(key)
, 和delete(key)
来操作单个键值对。 - 不支持
clear()
方法:WeakMap 没有清空所有键值对的方法。 - 垃圾回收时机不确定 :虽然 WeakMap 中的键值对会在键对象被垃圾回收后自动消失,但垃圾回收的具体发生时机是由 JavaScript 引擎决定的,你无法立即感知到。
何时选择 WeakMap vs. Map
-
选择 WeakMap 的情况:
你需要将数据(元数据、缓存、私有属性)与对象 关联起来,并且希望这些数据的生命周期跟随该对象 ,自动管理 ,避免内存泄漏。你也不需要遍历这些数据或知道其数量。
-
选择 Map 的情况:
你的键可以是任何类型 (包括原始值)。你需要遍历 键值对、需要知道数量 (
size
)、或者需要长期稳定地维护一组键值对集合,而不希望键被自动垃圾回收。
希望这些解释和示例能帮助你更好地理解和使用 WeakMap。
关注一下呗