JavaScript 闭包详解
JavaScript 闭包(Closure)详解
一、什么是闭包
**闭包(Closure)**是 JavaScript 中一个非常核心且常被问到的概念。简单来说:
闭包是一个函数,它可以“记住”并访问其定义时所在的词法作用域,即使这个函数在其作用域之外被调用。
换句话说,当一个函数“嵌套”在另一个函数中,并且内部函数引用了外部函数的变量时,就形成了闭包。
示例:
function outer() {
const name = 'hua';
function inner() {
console.log(name); // 可以访问外层函数的变量
}
return inner;
}
const fn = outer();
fn(); // 输出 "hua"
虽然 outer() 已经执行完毕,但 inner() 依然可以访问 outer() 中的 name。这就是闭包。
二、闭包的形成条件
要形成闭包,必须满足三个条件:
- 函数嵌套:存在一个函数嵌套在另一个函数内部;
- 外部函数变量被内部函数引用;
- 外部函数被执行并返回内部函数,从而让内部函数在外部作用域中仍然可访问。
function makeCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
makeCounter() 执行后返回了一个匿名函数,而这个匿名函数“记住”了 makeCounter() 的变量 count,即使 makeCounter() 已经结束。
三、闭包的原理(词法作用域 + 垃圾回收机制)
理解闭包的本质,离不开两个关键点:
1. 词法作用域(Lexical Scope)
JavaScript 的作用域在函数定义时确定,而不是在执行时确定。
这意味着内部函数天然就可以“捕获”外层函数的变量。
2. 垃圾回收机制(GC)
当外部函数执行完后,正常情况下其局部变量会被销毁。
但如果这些变量仍被内部函数引用,则它们会被“保留”在内存中,这样就形成了闭包。
四、闭包的常见应用场景
1. 封装私有变量
闭包可以用来模拟私有变量,避免外部直接访问:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
console.log(`存入 ${amount},当前余额:${balance}`);
},
withdraw(amount) {
if (amount <= balance) {
balance -= amount;
console.log(`取出 ${amount},当前余额:${balance}`);
} else {
console.log('余额不足');
}
}
};
}
const account = createBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.balance); // undefined,无法直接访问
2. 函数柯里化(Currying)
闭包是柯里化实现的核心:
function add(a) {
return function(b) {
return a + b;
};
}
console.log(add(1)(2)); // 3
add(1) 返回的函数记住了 a=1,这就是闭包在起作用。
3. 在循环中保存变量(经典面试题)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 3, 3, 3
原因是 var 没有块级作用域,所有回调共享同一个 i。
利用闭包可以解决:
for (var i = 0; i < 3; i++) {
(function(i) {
setTimeout(() => console.log(i), 100);
})(i);
}
// 输出: 0, 1, 2
或者使用 let(块级作用域)也能解决。
五、闭包的优点与缺点
✅ 优点
- 数据私有化:防止外部直接修改内部状态;
- 延长变量生命周期:可在函数外使用函数内的数据;
- 模块化开发:通过闭包封装逻辑,形成独立模块。
⚠️ 缺点
- 可能导致内存泄漏:闭包会持有变量引用,若使用不当,会使内存无法被释放;
- 调试困难:因为变量作用域链较长;
- 滥用闭包可能影响性能:尤其在循环中频繁创建闭包时。
六、闭包与内存泄漏问题
闭包会让一些本该被销毁的变量常驻内存。例如:
function test() {
let data = new Array(100000).fill('some data');
return () => console.log(data[0]);
}
const leak = test(); // data 未被释放
✅ 解决方案:
- 不再需要闭包时手动解除引用:
leak = null;
- 避免在全局环境长期保存闭包引用;
- 只在必要时使用闭包。
七、闭包与模块化模式
闭包可以帮助我们模拟模块系统:
const CounterModule = (function() {
let count = 0;
function add() {
count++;
console.log(count);
}
function reset() {
count = 0;
}
return {
add,
reset
};
})();
CounterModule.add(); // 1
CounterModule.add(); // 2
CounterModule.reset();
CounterModule.add(); // 1
这里的 count 相当于私有变量,只能通过暴露的函数访问。
八、闭包的底层实现机制(作用域链)
每个函数都有一个内部属性 [[Environment]],它指向定义该函数时的词法环境(Lexical Environment)。
当函数被调用时,会创建一个新的执行上下文,其中的作用域链包含:
- 当前函数的变量对象;
- 外部函数的变量对象;
- 全局对象。
当函数访问变量时,JS 引擎会沿着这条作用域链逐层查找。
九、总结
| 特性 | 说明 |
|---|---|
| 定义 | 闭包是函数与其外部词法环境的组合 |
| 本质 | 函数可以访问其定义时作用域中的变量 |
| 优点 | 数据私有化、延长生命周期、模块化封装 |
| 缺点 | 容易造成内存泄漏、调试困难 |
| 应用场景 | 计数器、私有变量、函数柯里化、模块化开发 |
十、思维导图
闭包(Closure)
├─ 概念:函数 + 外部环境
├─ 条件:函数嵌套 + 外部变量引用 + 外部函数执行
├─ 原理:词法作用域 + GC机制
├─ 应用:
│ ├─ 私有变量
│ ├─ 柯里化
│ ├─ 循环变量捕获
│ ├─ 模块化开发
├─ 优缺点
│ ├─ 优点:封装、延长生命周期
│ ├─ 缺点:内存泄漏、性能风险
└─ 注意事项:释放引用、合理使用如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录