闭包与对象引用的协同泄露源于双向强引用:闭包持对象引用,对象又反向持有闭包,形成gc不可回收的隐性循环。典型场景包括dom元素与闭包互引、组件ref未清理、短期对象被长期闭包持有、共享词法环境中无效驻留,以及weakmap/事件总线等载体未及时解绑。

识别闭包与对象引用的协同泄露,关键在于发现“双重持有”关系:闭包持有一个对象的引用,而该对象又反过来持有闭包(或其所在上下文)的引用,形成隐性循环链,使双方都无法被垃圾回收。
看是否构成双向强引用
当一个对象(如 DOM 元素、事件目标、自定义类实例)被闭包捕获,同时该对象自身又通过属性、回调字段等方式保存了该闭包(或包含它的函数),就构成协同泄露。这种结构在 GC 标记阶段无法被判定为“不可达”,即使逻辑上已无业务用途。
- 典型例子:给 DOM 元素挂载自定义属性 el.handler = closureFn,而 closureFn 内部又引用了 el(比如读取 el.dataset 或触发 el.style 修改)
- 另一个常见情况:Vue/React 组件中,将闭包赋值给 ref 对象的属性(如 ref.current.onClick = handleClick),而 handleClick 又访问组件 state 或 props —— 若 ref 没有随组件卸载清理,state 就被拖住
检查对象生命周期是否明显长于闭包用途
若某个对象本应短期存在(如一次性的弹窗容器、临时表单 DOM 节点),却因闭包持续持有它,导致其内存无法释放,就属于协同泄露信号。
- 例如:用 document.createElement('div') 创建浮层,闭包中保存了对该 div 的引用并绑定 click 监听器;后续调用 div.remove() 移除了它,但闭包变量未置 null,且该闭包又被全局计时器反复调用
- 此时 DOM 节点虽从文档树消失,仍被闭包强引用;而闭包又可能被定时器、Promise 回调等长期持有 —— 两者互相“托底”
观察词法环境是否膨胀且不可拆分
多个闭包共享同一外层作用域,其中仅部分闭包真正需要某些大对象,但所有闭包都因作用域绑定而间接持有它们,进一步加剧协同泄露风险。
- 比如一个工厂函数返回 { save, validate, log } 三个方法,它们共用 const cache = new Map(), config = loadBigConfig();但实际只有 save 用到 cache,log 只需 config 中的 level 字段
- 此时若 log 被挂到全局或作为事件回调长期存活,整个词法环境(含 cache 和完整 config)都会被保留,形成“无效驻留”
验证是否存在未清理的中间载体
协同泄露常借助第三方载体“搭桥”,比如 WeakMap、自定义缓存对象、全局事件总线、第三方库的注册表等。这些载体本身不直接泄漏,但若键值对未及时删除,就会让闭包和对象持续相互锚定。
- 例如:用 cacheMap.set(domEl, closure) 缓存处理函数,但 domEl 移除后未调用 cacheMap.delete(domEl)
- 再如:使用 EventEmitter.on('data', handler) 注册闭包,却未在销毁时调用 off('data', handler) —— handler 引用数据对象,而 emitter 又持有 handler,闭环形成










