闭包是函数与其定义时词法环境的组合,只要函数在其他作用域中仍能访问外部自由变量即构成闭包;常见于事件监听、定时器等场景,不当使用易致内存泄漏。

闭包是什么:函数记住了它诞生时的词法环境
闭包不是某种特殊函数,而是函数与其定义时所在作用域中变量的组合。只要一个函数在定义之后被带到其他作用域执行,且仍能访问定义时的自由变量,就形成了闭包。
常见误解是“返回函数才有闭包”,其实只要存在对外部变量的引用,哪怕没显式返回,也可能构成闭包(比如事件监听器、定时器回调)。
-
function makeCounter() { let count = 0; return () => ++count; }——count被内部箭头函数持续引用,makeCounter()每次调用都创建新闭包 -
for (var i = 0; i console.log(i), 100); }—— 所有回调共享同一个i(var 声明提升),输出全是3;这不是闭包失效,而是闭包捕获了同一变量绑定 - 改用
let i或(i => setTimeout(() => console.log(i), 100))(i)可让每次循环拥有独立绑定
闭包导致内存泄漏的典型场景
闭包本身不等于内存泄漏,但当它意外维持对大对象或 DOM 节点的强引用,且这些引用本该被释放时,就会阻碍垃圾回收(GC)。
浏览器 GC 主要靠「标记-清除」,若某个 DOM 元素已被 removeChild() 或从文档移除,但仍有闭包持有对该元素或其父级的引用,该元素及其整个子树就无法被回收。
立即学习“Java免费学习笔记(深入)”;
- 全局变量缓存 DOM 节点 + 闭包引用:
window.cache = document.getElementById('big-table'); const handler = () => console.log(cache.offsetHeight);—— 即使节点从页面移除,cache仍强引用它 - 未清理的事件监听器:
element.addEventListener('click', function handler() { /* 闭包内用了 this、外部变量 */ });—— 若未调用element.removeEventListener('click', handler),且element被移除,handler 闭包可能继续持有所需上下文 - 定时器长期运行并引用大对象:
const data = new Array(1000000).fill('heavy'); setInterval(() => console.log(data.length), 1000);——data永远无法释放
避免闭包引发内存泄漏的实操手段
关键不是消灭闭包,而是控制引用生命周期,确保不再需要时及时断开。
- 手动清理事件监听器:使用具名函数而非箭头函数注册,并在合适时机调用
removeEventListener;或用{ once: true }选项自动清理一次性监听 - 避免在闭包中直接引用 DOM 节点或大型数据结构;改用 ID、索引等轻量标识,需要时再查(如
const id = el.id; setTimeout(() => document.getElementById(id)?.remove(), 100)) - 及时解除全局引用:
window.cache = null;或使用WeakMap存储关联数据(WeakMap的键是弱引用,不阻止 GC) - 用
setTimeout/setInterval时,保存返回值并在不需要时调用clearTimeout/clearInterval - 现代框架(React/Vue)中,组件卸载时应清理副作用(
useEffect返回清理函数、beforeUnmount钩子)
怎么确认是不是闭包引起的泄漏?
不能靠猜。得用 Chrome DevTools 的 Memory 面板做快照比对。
操作流程:打开「Memory」→ 选「Heap snapshot」→ 点「Take heap snapshot」→ 执行疑似泄漏操作(如打开/关闭模态框)→ 再拍一张 → 对比两次快照,筛选 Closure 构造函数下的对象,看是否有异常增长、是否持有 DOM 节点(Detached HTMLDivElement 等)。
特别注意那些名字为 function () { ... }(匿名)、又出现在「Retained Size」前列的闭包实例——它们很可能就是泄漏源。
function createLeak() {
const bigData = new Array(500000).fill({ x: 1 });
const el = document.createElement('div');
el.innerHTML = 'test';
document.body.appendChild(el);
// 闭包引用了 bigData 和 el
el.onclick = () => console.log(bigData.length, el.tagName);
// ❌ 忘记清理:没有 removeEventListener,也没有移除 el
}
这种写法在反复调用后,每次都会留下无法回收的 bigData 和 el。真正难排查的,往往是跨模块、跨生命周期的隐式引用。











