JavaScript WeakMap和Map的区别:为什么用WeakMap做缓存
JavaScript 开发中,Map 和 WeakMap 长得很像,但它们在内存管理上有着天壤之别。如果不小心,用 Map 存储大量数据会导致内存泄漏,而 WeakMap 则能自动帮你清理垃圾。本文将直接通过对比和代码实操,教你如何在缓存场景下正确使用 WeakMap。
核心区别:强引用 vs 弱引用
要理解两者的不同,首先得搞懂“引用”的概念。
- Map (强引用):如果你把一个对象作为
key存进Map,只要这个Map对象还存在,垃圾回收器(GC)就绝对不会回收这个key对象。即使你在代码中已经把指向该对象的变量删掉了,它依然被Map死死抓住。 - WeakMap (弱引用):如果你把一个对象作为
key存进WeakMap,这只是一个“弱引用”。当你代码中没有其他地方引用这个对象时,垃圾回收器会无视WeakMap的存在,直接把这个对象回收。WeakMap里的这个键值对也会自动消失。
简单来说:Map 像是给数据上了把锁,WeakMap 像是放了一个随时会被风吹走的标记。
为了直观展示这一流程,请看以下内存回收的逻辑图:
graph LR
subgraph Map_Lifecycle ["Map: 强引用生命周期"]
A[创建对象 obj] -->|存储| B(Map实例)
B -- 强引用 --> A
C[删除外部引用
obj = null] --> A A -- 因为Map还抓着 --> D[对象无法回收
内存泄漏风险] end subgraph WeakMap_Lifecycle ["WeakMap: 弱引用生命周期"] E[创建对象 obj] -->|存储| F(WeakMap实例) F -. 弱引用 .-> E G[删除外部引用
obj = null] --> E E -- 没有强引用了 --> H((垃圾回收器回收)) H --> F F -- 自动移除 --> I[键值对消失] end
obj = null] --> A A -- 因为Map还抓着 --> D[对象无法回收
内存泄漏风险] end subgraph WeakMap_Lifecycle ["WeakMap: 弱引用生命周期"] E[创建对象 obj] -->|存储| F(WeakMap实例) F -. 弱引用 .-> E G[删除外部引用
obj = null] --> E E -- 没有强引用了 --> H((垃圾回收器回收)) H --> F F -- 自动移除 --> I[键值对消失] end
代码实操:Map 导致的内存问题
先看一个使用 Map 的错误缓存示范。
- 创建 一个普通对象
user,作为数据的载体。 - 实例化 一个
Map对象cache。 - 执行
cache.set(user, '用户数据')将数据存入缓存。 - 模拟 业务结束,将
user置为null(理论上我们不再需要这个对象了)。
// 1. 创建对象
let user = { name: 'Alice' };
// 2. 创建 Map 缓存
const mapCache = new Map();
// 3. 存入缓存
mapCache.set(user, '缓存数据: Alice的详细信息');
// 4. 外部删除引用
user = null;
// 检查:user 变成了 null,但 mapCache 里还有吗?
console.log(mapCache.keys().next().value);
// 输出: { name: 'Alice' }
// 结论:对象无法被回收,依然占用内存!
如果这种操作在高频场景(如每秒处理上千个请求)下发生,Map 会无限膨胀,最终导致内存溢出(OOM)。
代码实操:WeakMap 的自动清理
同样的逻辑,换成 WeakMap,情况截然不同。
- 创建 一个普通对象
user。 - 实例化 一个
WeakMap对象weakCache。 - 执行
weakCache.set(user, '用户数据')。 - 删除 外部引用
user = null。
// 1. 创建对象
let user = { name: 'Bob' };
// 2. 创建 WeakMap 缓存
const weakCache = new WeakMap();
// 3. 存入缓存
weakCache.set(user, '缓存数据: Bob的详细信息');
// 4. 外部删除引用
user = null;
// 检查:WeakMap 的 key 是弱引用
// 注意:WeakMap 没有 .keys() 方法,因为它的键随时可能被 GC 回收,无法遍历
// 此时,垃圾回收器会在适当的时候回收 { name: 'Bob' }
// weakCache 中的这一条数据也会自动消失
// 无法通过 size 或 keys 查看它,因为它已经“空”了
console.log(weakCache.has(user));
// 输出: false
功能对比表
为了方便查阅,以下是两者的详细对比,重点关注“键的类型”和“垃圾回收”。
| 特性 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任意类型(对象、基本类型、函数) | 仅限对象 |
| 引用类型 | 强引用(阻止垃圾回收) | 弱引用(不阻止垃圾回收) |
| 可遍历性 | 支持(keys(), values(), forEach) |
不可遍历(无 size 属性,无迭代器) |
| 常用场景 | 通用缓存、数据存储、需要统计数量的场景 | 对象关联数据、DOM节点缓存、私有属性 |
实战指南:用 WeakMap 做高性能缓存
在开发中,我们经常需要根据一个对象计算一些复杂的结果(比如 DOM 元素的坐标、复杂计算的结果等)。使用 WeakMap 是最佳实践,因为当对象销毁时,缓存也会随之销毁。
下面构建一个通用的“缓存处理函数”。
- 定义 一个
WeakMap变量resultCache,用于存储计算结果。 - 编写 处理函数
processData,接收targetObj对象。 - 判断 缓存中是否存在该对象的计算结果:调用
resultCache.has(targetObj)。 - 命中缓存:如果存在,调用
resultCache.get(targetObj)直接返回。 - 未命中缓存:
- 执行 复杂的计算逻辑(示例中用模拟耗时代替)。
- 调用
resultCache.set(targetObj, result)将结果存回缓存。 - 返回 计算结果。
// 1. 定义缓存容器
const resultCache = new WeakMap();
// 2. 编写带缓存的业务函数
function processData(targetObj) {
// 3. 检查缓存
if (resultCache.has(targetObj)) {
console.log('从缓存读取,节省计算时间');
// 4. 返回缓存结果
return resultCache.get(targetObj);
}
console.log('执行复杂计算...');
// 5. 模拟复杂计算(例如解析大文件、计算DOM位置等)
// 假设我们计算的是对象属性值的平方
const calculatedResult = targetObj.value * targetObj.value;
// 将结果与对象关联存入 WeakMap
resultCache.set(targetObj, calculatedResult);
return calculatedResult;
}
// --- 测试阶段 ---
const objA = { value: 10 };
const objB = { value: 20 };
// 第一次计算,会走计算逻辑
console.log(processData(objA)); // 输出: 100
// 第二次调用,直接读缓存
console.log(processData(objA)); // 输出: 100
// 处理另一个对象
console.log(processData(objB)); // 输出: 400
// --- 模拟对象销毁 ---
objA = null;
// 垃圾回收器运行后,objA 对应的缓存条目 (key: objA, value: 100) 会自动被清理
// resultCache 不需要手动写 delete 语句
何时必须使用 WeakMap
为了避免误用,请遵守以下判断标准。只有满足以下条件时,才应该使用 WeakMap:
- 你需要将数据关联到某个对象上,而不是仅仅存储数据。
- 你希望在对象被垃圾回收后,关联的数据自动消失(无需手动清理代码)。
- 你不需要遍历缓存,也不需要知道当前缓存里有多少条数据(没有
size属性)。
最典型的应用场景是 DOM 节点的元数据缓存。例如,你给页面上的某个 <div> 元素绑定了一些状态数据。如果用户把这个 <div> 删除了,你肯定希望那些状态数据也一起消失,否则页面越用越卡。此时,WeakMap 是唯一正确的选择。

暂无评论,快来抢沙发吧!