weakmap 是解决闭包内存泄漏最直接、最符合语言设计意图的方案,它通过弱引用键让垃圾回收器自然清理,避免手动管理;键必须为对象且不可遍历,适用于类私有状态和 dom 元素元数据缓存,但需注意 value 泄漏和键类型限制。

WeakMap 是解决闭包内存泄漏最直接、最符合语言设计意图的方案——它不靠手动清理,而是让垃圾回收器自然工作。
为什么闭包容易导致内存泄漏
闭包会持有对外部变量的强引用。比如一个函数返回内部函数,而这个内部函数又用了外部的大对象(如 DOM 节点、大型数组),那么只要这个内部函数还被引用着,外部大对象就无法被回收,哪怕它早就该被释放了。
典型场景包括:事件监听器里闭包引用了节点和数据、模块缓存中长期持有实例、私有状态用普通对象挂载等。
WeakMap 的关键机制正好对症
WeakMap 的键必须是对象,且是弱引用——它不会阻止垃圾回收。只要这个对象在别处没有其他强引用,哪怕 WeakMap 里还存着它,GC 也能把它连同对应的值一起清理掉。
- 键只能是对象(DOM 元素、class 实例天然适用)
- 没有 size、keys()、entries() 等遍历方法,避免意外延长生命周期
- 值(value)本身仍需注意:WeakMap 不管 value 是否被其他地方引用,只保证键一消失,整个条目就失效
两种最常用的实际写法
1. 给类实例加私有状态
替代传统闭包或 this 上挂属性的方式,避免污染实例:
const privateState = new WeakMap();
class Button {
constructor(el) {
privateState.set(this, { loading: false, retryCount: 0 });
this.el = el;
}
startLoading() {
const state = privateState.get(this);
state.loading = true;
}
}
// 当 new Button(el) 实例被丢弃,privateState 中对应条目自动消失
2. 缓存 DOM 元素的业务数据
比挂在 element.dataset 或自定义属性更安全,节点移除后无需手动清理:
const nodeMeta = new WeakMap();
function setButtonStatus(btn, status) {
nodeMeta.set(btn, { ...nodeMeta.get(btn), ...status });
}
function getButtonStatus(btn) {
return nodeMeta.get(btn) || {};
}
// document.body.removeChild(btn) 后,nodeMeta 里这条记录自动失效
使用时要注意的坑
WeakMap 不是万能胶,用错反而掩盖问题:
- 别用字符串、数字当键——会直接报 TypeError
- 不要指望用 WeakMap 存“全局配置”或需要遍历的数据,它天生不可枚举
- 如果 value 是个带定时器的对象,或被其他闭包长期持有,那 value 本身仍可能泄漏
- 推荐统一用模块级单例 WeakMap,而不是每个函数里都 new 一个











