闭包导致内存泄漏的核心在于“不该留却一直留着”的强引用。典型场景包括:1. 事件监听器绑定闭包后未解绑;2. 定时器回调闭包未清除;3. 循环中为每个项创建冗余闭包;4. 闭包被挂载到全局或长生命周期对象上。

闭包本身不是问题,问题出在它“不该留却一直留着”的引用关系上。当闭包长期持有对大对象、DOM 元素或全局变量的强引用,而这些对象本该被释放时,内存泄漏就发生了。以下是实际项目中最常踩坑的几类代码模式。
1. 事件监听器绑定闭包后未解绑
这是最典型的泄漏场景:闭包作为回调函数绑定到 DOM 元素,但元素被移除或组件卸载后,监听器仍挂着,闭包连带引用的外部数据全部滞留内存。
- 常见写法:
element.addEventListener('click', () => { console.log(data); });—— data 是大型对象或整个组件状态 - 风险点:没有配套的
removeEventListener,尤其在单页应用(SPA)中组件反复挂载/卸载时,监听器越积越多 - 关键识别信号:Chrome DevTools 的 Memory 面板中看到 detached DOM 节点仍被 JS 对象引用
2. 定时器回调形成闭包并长期运行
用 setInterval 或 setTimeout 启动的闭包,若未在合适时机清除,会持续持有对外部作用域的引用,且定时器本身无法被 GC。
- 典型反例:
const timer = setInterval(() => { console.log(userData.profile); }, 3000);—— 组件销毁后 timer 仍在跑,userData无法释放 - 特别危险的是:把 timer ID 存在闭包外(如全局或模块级变量),但忘记调用
clearInterval(timer) - 更隐蔽的情况:闭包内引用了整个 Vue/React 组件实例(比如
this或props),导致整个组件树无法回收
3. 循环中为每个项创建独立闭包
在 for 或 forEach 中为多个 DOM 元素分别绑定事件处理函数,每个都形成一个闭包,且多数情况下都意外捕获了循环变量或共享的大对象。
- 经典陷阱:
for (let i = 0; i console.log(i); }—— 表面看用let避免了变量提升,但每个闭包仍各自持有一份i和对items数组的引用 - 放大效应:100 个元素 → 100 个闭包 → 100 份冗余引用,尤其当闭包还引用
data这类大对象时,内存占用线性增长 - 真实项目中常见于表格行操作、列表项点击、轮播图按钮等批量交互场景
4. 闭包被挂载到全局或长生命周期对象上
把闭包赋值给 window、globalThis、模块顶层变量或第三方库的缓存对象,等于人为延长其生命周期至整个页面存在期间。
- 例如:
window.cacheFn = createClosure();,其中createClosure内部持有new Array(1e6)等大数据 - 再如:将闭包存入
Map或WeakMap但未及时delete,特别是键是 DOM 元素时,WeakMap 也无法起效(因 DOM 元素未被真正释放) - 这类泄漏不易察觉,因为代码看起来“只是存了个函数”,但背后拖着整个作用域链











