闭包导致内存泄漏的根源是词法环境因强引用链长期驻留。典型情况包括:闭包引用大对象且未清理、多闭包共享环境致“连坐”、隐式全局绑定、挂载到长生命周期对象、以及闭包与对象间循环引用。

闭包本身不是问题,问题出在它让本该退出作用域的变量“赖着不走”。当外部函数执行完、执行上下文早已出栈,其词法环境却因闭包的强引用链持续驻留——这才是内存无法释放的根源。
闭包持有了不该长期存在的大对象
最典型的极端情况:闭包内部直接引用了巨型数组、超长字符串、完整 DOM 树节点或未序列化的大型 JSON 对象。这些数据本应在函数退出后立即被回收,但只要闭包还被某个全局变量、定时器回调、事件监听器或长期存活的模块引用,整个词法环境(连带所有局部变量)就始终处于“可触达”状态,GC 完全跳过它们。
- 比如:
const handler = () => console.log(largeData)被注册为按钮点击事件,之后按钮被移除但没调用removeEventListener,largeData就永远卡在内存里 - 再如:一个模块导出的闭包被其他模块反复引用,而它内部捕获了初始化时创建的缓存 Map,这个 Map 会随模块生命周期无限膨胀
多个闭包共享词法环境却只用其中一小部分
一个外层函数定义了 5 个内部函数,其中只有 1 个真正用到了 hugeImageBuffer,其余 4 个完全没碰它。但因为 JS 引擎按作用域整体保留词法环境,hugeImageBuffer 会被全部 5 个闭包“连坐”——哪怕你只保留第 2 个闭包,其余 4 个早已弃用,hugeImageBuffer 依然无法释放。
- 这不是代码写错,而是引擎实现机制决定的:词法环境是“打包保存”的,不支持按变量粒度拆解
- 尤其在 React 组件、Vue setup 或类工厂模式中,这种“共享但非必需”的捕获非常隐蔽
闭包被意外挂载到全局或长生命周期对象上
比显式赋值给 window.xxx 更危险的是隐式泄漏:比如忘记加 let/const 声明,导致变量自动绑定到全局;或者把闭包作为属性塞进某个长期存在的单例对象、路由守卫、日志中间件里,而后续没有清理入口。
- 示例:
function init() { cache = createCache(); return () => cache.get('key'); }——cache是隐式全局,永不释放 - 又如:在 SPA 页面切换时,旧组件的闭包监听器仍挂在全局事件总线里,新页面加载后老监听器照常执行并持有旧组件数据
循环引用 + 闭包双重锁定
当闭包内部引用的对象反过来又持有该闭包(比如对象方法是闭包本身),就形成双向强引用。现代 JS 引擎虽能处理纯对象间的循环引用,但一旦混入闭包,词法环境这层额外绑定会让 GC 更难判定“是否真不可达”。
- 典型场景:WebSocket 回调闭包引用了当前实例,而实例的
onmessage又被设为该闭包;断开连接后若未手动重置,两者互相拖住 - 更隐蔽的是:闭包捕获了
this,而this又通过原型链、属性或事件绑定反向引用了闭包











