一、概念
闭包是能够访问并记住外部函数作用域的函数,即使外部函数已经执行完毕。
二、核心机制
- 词法作用域:函数在定义时确定作用域链,而非执行时。闭包通过作用域链访问外部变量。
- 环境保留:当内部函数被返回或传递时,外部函数的作用域会被保留,不会被垃圾回收。
function outer() {
let count = 0; // 外部函数的变量
function inner() { // 内部函数(闭包)
count++; // 访问外部变量
console.log(count);
}
return inner; // 返回内部函数
}
const closureFn = outer(); // outer() 执行完毕,但 count 被 inner 引用,不会销毁
closureFn(); // 输出 1
closureFn(); // 输出 2(count 状态被保留)
三、核心特点
- 持久化变量:闭包中的外部变量会一直存在,直到闭包被销毁。
- 私有性:可隐藏数据,仅通过闭包暴露特定操作(类似私有变量)。
- 跨作用域访问:闭包可以访问定义时的整个作用域链(自身、外部函数、全局作用域)。
四、用途
- 模块化:封装私有变量,只暴露接口。
function createCounter() {
let privateCount = 0; // "私有"变量,外部无法直接访问
return {
increment: function() {
privateCount++;
},
getCount: function() {
return privateCount;
}
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出 1
console.log(counter.privateCount); // undefined(无法直接访问)
- 事件处理:在循环中保留变量状态。
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
// 输出0,1,2(使用闭包保留每次循环的i值)
}
五、示例对比:闭包 vs 非闭包
- 例1:是闭包(满足所有条件)
function outer() {
let a = 10;
function inner() {
console.log(a); // 引用外部变量a
}
return inner; // 内部函数被返回
}
const func = outer();
func(); // 在外部作用域调用,且此时outer已执行完毕 → 闭包
- 例2:非闭包(未引用外部变量)
function outer() {
let a = 10;
function inner() {
console.log("Hello"); // 仅用自身作用域的变量
}
return inner;
}
const func = outer();
func(); // 未引用外部变量 → 不是闭包
- 例3:非闭包(内部函数未脱离外部作用域)
function outer() {
let a = 10;
function inner() {
console.log(a);
}
inner(); // 在outer内部直接调用 → 未脱离外部作用域
}
outer(); // 输出10 → 此时inner不构成闭包
- 例4:闭包(异步调用)
function outer() {
let a = 10;
setTimeout(function inner() {
console.log(a); // 在outer执行完毕后被调用 → 闭包
}, 1000);
}
outer(); // 输出10(延迟1秒)
快速判断步骤
- 是否有函数嵌套?
如:inner是否在outer内定义? - 内部函数是否引用了外部变量?
如:inner是否使用了outer中的变量(如a)? - 内部函数是否在外部函数之外被调用?
如:inner是否被返回、传递给setTimeout,或在全局作用域调用?
六、如何销毁闭包并释放变量
- 手动解除引用
将保存闭包的变量设为 null,切断闭包与外部变量的联系。
function createClosure() {
let data = "敏感数据";
return function() { console.log(data); };
}
let closure = createClosure();
closure(); // 正常使用
closure = null; // 解除引用,data可被回收
- 避免长期持有闭包
在不需要时立即释放闭包,而非长期存储。
// 错误:闭包长期存在
const cache = [];
function saveClosure() {
let data = "数据";
cache.push(() => console.log(data)); // 闭包被长期引用
}
// 正确:仅在需要时创建闭包
function processData() {
let data = "临时数据";
const closure = () => console.log(data);
closure();
}
- 避免循环引用
function createLeak() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2; // obj1 引用 obj2
obj2.ref = obj1; // obj2 引用 obj1(循环引用)
return function() {
console.log(obj1, obj2); // 闭包保持对 obj1 和 obj2 的引用
};
}
const leakyClosure = createLeak();
leakyClosure = null; // 即使解除引用,obj1 和 obj2 仍然互相引用,无法被 GC!
//解决方法:
//1. 手动断开循环引用(obj1.ref = null; obj2.ref = null)。
//2. 使用 WeakMap 或 WeakSet 存储对象。
- 清除事件监听/定时器
若闭包用于事件或定时器,需显式移除。
// 闭包绑定事件
function init() {
let element = document.getElementById("btn");
let data = "状态数据";
element.addEventListener("click", () => {
console.log(data); // 闭包引用data和element
});
}
// 销毁时移除事件
function destroy() {
element.removeEventListener("click", handler); // 移除事件
element = null; // 解除DOM引用
}
- 使用弱引用(WeakMap/WeakSet)
弱引用允许外部对象被回收,避免内存泄漏。
const weakMap = new WeakMap();
function registerObject(obj) {
let privateData = "内部数据";
weakMap.set(obj, privateData); // 弱引用:obj销毁后,数据自动释放
}
let obj = { id: 1 };
registerObject(obj);
obj = null; // 垃圾回收可回收obj及其关联数据
七、内存泄漏检测工具
- Chrome DevTools Memory Tab:
使用堆快照(Heap Snapshot)分析闭包引用。
查找未释放的闭包函数(如 Closure 标识)。 - Performance Monitor:
监控内存使用,发现未回收的闭包。
总结
闭包的核心特征是:
内部函数在定义时捕获了外部作用域的变量,并在外部函数执行完毕后仍能访问这些变量。通过以上方法,可以判断代码中是否存在闭包以及销毁闭包。