WeakMap 适合单例模式中按实例隔离状态,因其键为弱引用对象,可自动清理避免内存泄漏;而普通 Map 或属性挂载会导致全局共享或需手动清理。

WeakMap 是 JavaScript 中用于存储对象键值对的集合,其键必须是对象,且对键的引用是弱引用——当键对象被垃圾回收时,对应的键值对也会自动被移除。这使得 WeakMap 成为在不干扰对象生命周期的前提下,为对象附加私有、临时上下文数据的理想工具,尤其适合复杂单例模式中“按实例隔离状态”的场景。
为什么单例模式需要 WeakMap 而不是普通 Map 或属性挂载
复杂单例(如一个全局管理器类)可能被多个模块或组件多次“获取”(通过 getInstance()),但实际只存在一个实例。若想为每个调用方或每个使用上下文(比如不同 React 组件实例、不同请求上下文、不同用户会话)单独保存状态,就不能把数据直接挂在单例本身上——那会变成全局共享;也不能用普通 Map 存储 {caller: data},因为 caller(比如某个组件实例)若未手动清理,会导致内存泄漏。
WeakMap 的弱引用特性恰好解决这个问题:你把 caller 对象作为 key,它的上下文数据作为 value;只要 caller 被销毁(比如组件卸载),WeakMap 自动丢弃该条目,无需手动清理。
典型用法:为不同调用者隔离上下文数据
假设你有一个单例 ResourceManager,它需为不同 React 组件实例缓存各自的资源加载状态(如 loading/error/data)。你不想污染组件,也不希望状态跨组件泄漏:
立即学习“Java免费学习笔记(深入)”;
class ResourceManager {
constructor() {
// 私有 WeakMap,仅本实例可访问
this.#contextCache = new WeakMap();
}
#contextCache = new WeakMap();
// 获取当前 caller(如组件实例)专属的上下文对象
#getOrCreateContext(caller) {
let ctx = this.#contextCache.get(caller);
if (!ctx) {
ctx = { loading: false, data: null, error: null };
this.#contextCache.set(caller, ctx);
}
return ctx;
}
loadData(caller, resourceId) {
const ctx = this.#getOrCreateContext(caller);
ctx.loading = true;
return fetch(`/api/${resourceId}`)
.then(res => res.json())
.then(data => {
ctx.data = data;
ctx.loading = false;
return data;
})
.catch(err => {
ctx.error = err;
ctx.loading = false;
throw err;
});
}
// 可选:提供只读快照,避免外部篡改
getContextSnapshot(caller) {
const ctx = this.#contextCache.get(caller);
return ctx ? { ...ctx } : null;
}
}
// 全局单例
const resourceManager = new ResourceManager();
export default resourceManager;
使用时(如在 React 组件中):
function MyComponent({ id }) {
useEffect(() => {
// 传入 this(即组件实例)作为 caller
resourceManager.loadData(this, id).catch(console.error);
}, [id]);
// 渲染时读取专属状态
const snapshot = resourceManager.getContextSnapshot(this);
return (
<div>
{snapshot?.loading ? 'Loading...' : JSON.stringify(snapshot?.data)}
</div>
);
}
进阶技巧:配合 Symbol 和闭包强化封装性
WeakMap 本身不能暴露给外部,否则破坏封装。推荐两种增强方式:
- 用闭包隐藏 WeakMap:像上面示例那样,在构造函数作用域内声明 WeakMap,外部无法访问;
-
用 Symbol 做“伪私有字段”辅助:如果单例需支持子类扩展,可在基类中定义
Symbol('context'),让子类通过该 symbol 访问自己的 WeakMap 实例,而非共享同一张表; - 结合 FinalizationRegistry(ES2021+)做清理后钩子:虽然 WeakMap 自动清理,但有时你需要在 caller 销毁后执行日志、上报或异步释放资源,可用 registry 监听并触发回调(注意:registry 不保证及时性,仅作补充)。
注意事项与边界情况
WeakMap 并非万能,使用时需注意:
-
键只能是对象:不能用字符串、数字、null、undefined 作 key;若需支持原始值,需包装成对象(如
{ id: 'abc' }),但要确保每次传入的是同一引用,否则无法命中; -
无法遍历和查询大小:WeakMap 没有
.keys()、.size等方法,调试时可用console.dir(map)查看(部分环境支持),但生产中不应依赖; - 不能替代真正的依赖注入或上下文 API:它适合轻量级、临时性、对象粒度的状态绑定;若逻辑复杂、需跨层级传递、或涉及异步链路追踪,应考虑 Context API、AsyncLocalStorage(Node.js)或专用状态容器;
- 单例自身不能作为 WeakMap 的 key 来存自己的状态:因为单例长期存活,WeakMap 条目永远不会被回收,失去意义;WeakMap 的价值在于绑定“短命对象”。


















