一. 啥叫闭包?
- 先来看一下经典的示例:
javascript
function createCounter() {
let count = 0; // 外部函数变量
return function() {
count++; // 内部函数访问外部变量
return count;
};
}
const counter = createCounter();
console.log(counter());
// 1 console.log(counter());
// 2 (保留对count的引用)
createCounter 执行过程解析:
-
createCounter()
执行完毕,但返回的匿名函数仍持有对count
的引用 -
count
变量不会被销毁,形成"私有状态" -
每次调用
counter()
都修改同一个count
这里,createCounter
返回的方法就形成了闭包,保留了对 count
变量的引用。count
变量不会被垃圾回收,因为它被闭包"记住"了
那么就可以简单的引出闭包了
当一个函数(内部函数)在其词法作用域之外被调用时,它仍然能访问原作用域中的变量------这种"跨作用域的记忆能力"就是闭包。
下面找一个闭包的定义哈
一个函数与其词法环境中所有变量的引用共同组成的"闭包对象"
简单点就是 闭包 = 函数 + 其创建时的词法作用域环境
那么下面的闭包关键点以及形成条件就是进一步理解闭包的关键啦。
闭包的关键点解析:
-
词法作用域(Lexical Scope)
JavaScript 函数的作用域在定义时确定(而非执行时),它能访问定义位置的外层作用域变量。
-
闭包的形成条件
-
嵌套函数(内部函数访问外部函数变量)
-
内部函数被导出到外部作用域(如通过返回值、事件回调等)
-
-
生命周期特性
即使外部函数已执行完毕,闭包仍能保留其词法作用域中的变量(不会被垃圾回收)。
闭包的形成条件:
要形成闭包,需满足两个关键条件:
-
存在嵌套函数:外层函数内部定义了一个内层函数;
-
内层函数被外部引用:内层函数被返回或传递到外层函数之外执行
二、闭包的实际应用场景:
- 数据封装(模拟私有变量)
javascript
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
return {
deposit: (amount) => balance += amount,
withdraw: (amount) => balance -= amount,
getBalance: () => balance
};
}
const account = createBankAccount(100);
account.withdraw(30);
console.log(account.getBalance()); // 70 (外部无法直接访问balance)
2. 函数工厂
javascript
function powerFactory(exponent) {
return function(base) {
return base ** exponent;
};
}
const square = powerFactory(2);
console.log(square(5)); // 25
- 事件处理(保留上下文)
javascript
function setupButton() {
const button = document.getElementById('myBtn');
let clickCount = 0;
button.addEventListener('click', () => {
clickCount++;
console.log(`Clicked ${clickCount} times`);
});
}
// 每次点击都访问同一个clickCount
4. 模块模式(Modern JS 已被 class 替代)
ini
const Calculator = (() => {
let memory = 0;
return {
add: (x) => memory += x,
getMemory: () => memory
};
})();
5. 函数柯里化(Currying)
scss
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return (...nextArgs) => curried(...args, ...nextArgs);
}
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出 6
三、 闭包的工作原理
JavaScript 引擎通过作用域链(Scope Chain)管理变量的访问。当函数执行时,会创建一个执行上下文(Execution Context),其中包含:
-
词法环境(Lexical Environment):存储当前作用域的变量和函数声明;
-
变量环境(Variable Environment) :存储
var
声明的变量(ES6 后与词法环境合并); -
this 绑定 :函数的
this
指向。
当内层函数被外部调用时,它的作用域链不会被销毁,而是保留了外层函数作用域的引用。因此,即使外层函数已执行完毕,内层函数仍能通过作用域链访问外层函数的变量。
四、常见误区与注意事项:内存与性能
闭包虽然强大,但需注意以下问题:
1. 内存泄漏风险
闭包会长期保留对外部作用域变量的引用,导致这些变量无法被垃圾回收。如果闭包中引用了大对象(如 DOM 元素),可能导致内存占用过高。
javascript
function heavyProcess() {
const bigData = new Array(1000000); // 大对象
return function() {
// 即使不再需要bigData,它仍被保留在内存中!
};
}
解决方法:
-
在闭包不再需要时,手动解除引用(如将闭包变量置为
null
); -
避免在循环中不必要地创建闭包;
-
使用块级作用域(
let
/const
)限制变量作用范围。
2. 循环中的闭包陷阱
css
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3 (不是预期的0,1,2)
解决方案:
-
使用
let
(块级作用域)for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 0,1,2 }
-
立即执行函数(IIFE)
for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); // 0,1,2 })(i); }
3. 变量共享问题
闭包中引用的外部变量是引用绑定(而非值拷贝),多个闭包可能共享同一个变量,导致意外行为。
javascript
// 错误示例:所有按钮点击都输出 3(闭包共享同一个 i)
for (var i = 0; i < 3; i++) {
document.querySelectorAll('button')[i].addEventListener('click', function() {
console.log('按钮 ' + i + ' 被点击'); // i 是循环结束后的值 3
});
}
// 正确示例:用闭包保留当前 i 的值(ES5 方案)
for (var i = 0; i < 3; i++) {
(function(j) { // 立即执行函数创建闭包,保存当前的 j=i
document.querySelectorAll('button')[j].addEventListener('click', function() {
console.log('按钮 ' + j + ' 被点击'); // 输出 0、1、2
});
})(i);
}
// ES6 方案:用 let 声明 i(块级作用域自动形成闭包)
for (let i = 0; i < 3; i++) {
document.querySelectorAll('button')[i].addEventListener('click', function() {
console.log('按钮 ' + i + ' 被点击'); // 输出 0、1、2
});
}
五、总结:闭包的本质
闭包的本质是函数对其词法环境的"记忆"能力。它让函数突破了执行时的作用域限制,能够访问定义时的环境。这一特性是 JavaScript 实现模块化、私有变量、函数式编程(如柯里化)的核心基础。
理解闭包的关键是掌握:
-
词法作用域决定了闭包的"记忆范围";
-
闭包通过保留作用域链实现跨作用域访问;
-
合理使用闭包能提升代码灵活性,但需注意内存管理。