目录
1、定义
- 闭包:
带着外部变量的内部函数。 即使外部函数执行完退出了,这个内部函数依然能访问和操作,它创建时所在的外部函数里的变量,不会随着外部函数的结束而丢失这些变量的访问权。 - 严谨的表述:当函数被创建并返回(或被保存)时,它会携带自己的词法环境(也就是创建它时所处的外部作用域),这个函数 + 它携带的词法环境,就构成了闭包。
2、示例1
1、外部函数就像一间教室,里面的变量是教室里的桌椅;内部函数就像一个学生,闭包就是这个学生离开教室时,把桌椅的 "使用权凭证" 带在了身上,哪怕教室已经锁门(外部函数执行完毕),学生依然能使用这些桌椅(访问外部变量)。
2、
内部函数(学生) + 外部变量的访问引用(桌椅的使用权凭证) = 闭包。3、闭包并没有复制外部变量,只是持有了外部变量的访问引用。
js
// 外部函数
function outerFunc() {
// 外部函数作用域中的变量(仅在 outerFunc 内部可访问,正常情况下外部无法触及)
let outerVar = "我是外部函数的变量";
// 内部函数(闭包的核心:这个内部函数就是闭包载体)
function innerFunc() {
// 内部函数访问了外部函数的变量 outerVar
console.log(outerVar);
}
// 外部函数返回内部函数(让内部函数可以在外部被调用)
return innerFunc;
}
// 1. 调用外部函数,得到内部函数(此时 outerFunc 已经执行完毕,按常理其内部变量应被销毁)
const myClosure = outerFunc();
// 2. 调用内部函数(依然能访问到 outerFunc 中的 outerVar,这就是闭包的效果)
myClosure(); // 输出:我是外部函数的变量
3、分析
- outerFunc 执行完毕后,按照 JS 的垃圾回收机制,它的内部变量 outerVar 应该被回收销毁。
- 但因为 innerFunc 访问了 outerVar,且 innerFunc 被返回并赋值给了 myClosure(保留了引用),所以 outerVar 不会被销毁,会被 innerFunc 携带保存。
- 当调用 myClosure() 时,innerFunc 依然能从自己携带的「词法环境」中找到 outerVar,这就是闭包在起作用。
4、特性
- 访问外部变量:内部函数可以访问外部函数的变量,甚至是外部函数的外部函数的变量(词法作用域链)。
- 变量持久化:外部函数执行完毕后,其内部变量不会被垃圾回收,而是被闭包持有,直到闭包本身被销毁(比如 myClosure = null)。
- 隔离作用域:闭包可以创建私有变量,避免变量污染全局作用域,这是闭包最常用的实际场景。
4.1、示例2:闭包实现私有变量(实用场景)
js
// 实现一个简单的计数器,变量 count 仅能通过 increment 和 getCount 操作,无法直接修改
function createCounter() {
// 私有变量:仅在 createCounter 内部可访问,全局无法直接触及
let count = 0;
// 返回一个对象,包含两个方法(这两个方法都是闭包,持有 count 的引用)
return {
// 自增方法
increment: function() {
count++;
},
// 获取当前值方法
getCount: function() {
return count;
}
};
}
// 创建计数器实例1
const counter1 = createCounter();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 输出:2
// 创建计数器实例2(与 counter1 互不干扰,各自持有独立的 count 变量)
const counter2 = createCounter();
console.log(counter2.getCount()); // 输出:0
示例中,count 就是私有变量,全局无法直接修改,只能通过 increment() 和 getCount() 操作,避免了变量污染,这也是闭包在实际开发中的核心用途之一。
5、注意事项
- 内存泄漏风险:如果闭包被长期保存(比如赋值给全局变量),且持有大量无用的外部变量,会导致这些变量无法被垃圾回收,从而造成内存泄漏。
解决方案:不需要使用闭包时,手动解除引用(比如 myClosure = null)。
- 变量共享问题:在循环中创建闭包时,容易出现变量共享导致结果不符合预期,需要注意。
5.1、循环中的闭包坑
javascript
// 错误示例:循环中创建闭包,期望输出 0、1、2,实际输出 3、3、3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出:3、3、3
}, 1000);
}
原因分析:
- var 声明的变量没有块级作用域,i 是全局变量,循环结束后 i 的值为 3。
- 定时器中的函数是闭包,它访问的是全局的 i,当定时器执行时,循环已经结束,i 已经是 3,所以输出 3 次 3。
解决方案:
- 使用 let 声明变量(let 有块级作用域,每次循环都会创建一个独立的 i)。
javascript
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出:0、1、2
}, 1000);
}
- 使用立即执行函数(IIFE)创建独立作用域
javascript
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出:0、1、2
}, 1000);
})(i);
}
6、总结
闭包的成立必须满足两个核心条件(缺一不可):
- 存在嵌套函数(内部函数嵌套在外部函数中)。
- 内部函数被「带出」外部函数的作用域(比如被返回、被赋值给外部变量、被传递给其他函数作为参数),且内部函数访问了外部函数的变量。
- 示例:不满足条件的 "伪闭包"(不算真正的闭包)
javascript
function outerFunc() {
let outerVar = "外部变量";
// 内部函数仅在外部函数内部调用,没有被带出外部作用域
function innerFunc() {
console.log(outerVar);
}
// 直接在外部函数内部执行内部函数,不是闭包
innerFunc();
}
outerFunc(); // 输出:外部变量(这只是普通的嵌套函数,不是闭包)
示例中,虽然有内部函数访问外部变量,但内部函数没有被带出外部函数的作用域,外部函数执行完毕后,outerVar会被正常回收,不构成闭包。