闭包
闭包(Closure)是 JavaScript 中最强大的特性之一,也是函数式编程的核心概念。
🔍 闭包是什么?
闭包 = 函数 + 创建时的环境
当一个函数可以记住并访问所在的词法作用域时,就产生了闭包。
🧩 闭包三要素
- 🏗️ 函数嵌套 - 一个函数内部定义另一个函数
- 🔗 变量引用 - 内部函数引用外部函数的变量
- 🚀 外部执行 - 内部函数在外部函数之外被调用
💡 基础示例
🎯 示例1:计数器闭包
function createCounter() {
let count = 0; // 🎒 被闭包"背走"的变量
return function() {
count++; // 🏷️ 每次调用都记住这个count
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
🎯 示例2:立即执行函数(IIFE)
const uniqueId = (function() {
let id = 0; // 🔒 私有变量
return function() {
return id++; // 📈 每次调用自增
};
})();
console.log(uniqueId()); // 0
console.log(uniqueId()); // 1
⚙️ 闭包的工作原理
闭包的核心原理是 JavaScript 的作用域链。当一个内部函数被返回并在外部作用域中调用时,它依然保持对它创建时的作用域的引用,因此可以访问该作用域中的变量。
这意味着闭包可以访问并“捕获”它外部函数中的变量,即使外部函数已经执行完毕。
- 作用域链 🌐
- 函数在定义时会记住自己的词法作用域
- 形成一条作用域链条
- 变量保持 🧳
- 即使外部函数执行完毕
- 被引用的变量依然存在
- 内存不释放 🚫🗑️
- 被闭包引用的变量不会被垃圾回收
🛠️ 闭包的四大用途
🔒 封装私有变量
//创建银行账户
function createBankAccount() {
let balance = 0; // 🏦 私有变量(只能在闭包内访问)
return {
deposit(amount) { balance += amount }, // 存款方法
withdraw(amount) { balance -= amount }, // 取款方法
getBalance() { return balance } // 查询余额方法
};
}
const account = createBankAccount();
account.deposit(100); // 存入100元
console.log(account.getBalance()); // 显示余额 100
🧩 模块模式
通过闭包,可以模拟模块的概念,将变量和函数封装在一个函数作用域内,不污染全局作用域。
const game = (function() {
let score = 0; // 🎮 游戏分数(私有变量)
function addPoints(points) {
score += points; // 内部加分函数
}
return {
play() { // 游戏玩法
addPoints(10); // 每次玩加10分
console.log(`得分: ${score}`);
},
reset() { // 重置游戏
score = 0; // 分数归零
}
};
})();
game.play(); // 输出: 得分: 10
game.play(); // 输出: 得分: 20(再次调用会继续累加)
game.reset(); // 重置分数为0
game.play(); // 输出: 得分: 10(重置后重新开始)
💡关键概念解析:
- IIFE (立即调用函数表达式) -
(function(){...})()
创建了一个独立作用域 - 闭包 - 内部函数(
addPoints
)可以访问外部函数的变量(score
) - 模块模式 - 只暴露
play
和reset
方法,保持score
的私有性
✂️ 函数柯里化
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier; // 🔢 闭包记住了乘数参数
};
}
const double = createMultiplier(2); // 创建一个乘以2的函数
console.log(double(5)); // 输出: 10 (5×2)
const triple = createMultiplier(3); // 创建一个乘以3的函数
console.log(triple(5)); // 输出: 15 (5×3)
💡关键概念解析:
- 高阶函数:
createMultiplier
是一个返回函数的函数(高阶函数) - 闭包特性:返回的函数记住了
multiplier
参数(即使外部函数已执行完毕) - 应用场景:工厂模式创建特定功能的函数
⏱️延迟执行
function delayedAlert(msg, time) {
setTimeout(() => {
// 🕒 闭包记住了msg参数,即使外部函数已执行完毕
console.log(msg);
}, time);
console.log("外部函数执行完毕")
}
delayedAlert("延迟执行参数", 1000);
🖱️ 事件处理
function setupButtons() {
const colors = ['red', 'green', 'blue']; // 可用颜色数组
// 为每个颜色创建按钮事件监听
colors.forEach((color, index) => {
// 获取对应ID的按钮元素
document.getElementById(`btn-${index}`)
// 添加点击事件监听器
.addEventListener('click', () => {
// 🎨 通过闭包记住当前迭代的color值
console.log(`选择了 ${color}`);
});
});
}
/*
实际HTML结构示例:
<button id="btn-0">红色</button>
<button id="btn-1">绿色</button>
<button id="btn-2">蓝色</button>
*/
⚠️闭包注意事项
🧠 内存泄漏风险
闭包会捕获并“记住”外部函数中的局部变量,即使外部函数已经执行完毕。这是因为闭包仍然持有对这些变量的引用,这样变量不会被垃圾回收机制回收,从而继续存在于内存中。
function createHeavyObject() {
const bigData = new Array(1000000).fill('*'); // 🏋️ 大数据
return function() {
console.log('可能泄漏内存');
// bigData 一直被引用,无法释放
};
}
✅ 正确做法
JavaScript 的垃圾回收机制会回收那些不再被引用的对象
function safeClosure() {
const data = getLargeData();
function process() {
// 使用data...
}
// 使用完后清除引用
data = null;
return process;
}
🎯 经典面试题解析
🔄 循环中的闭包问题
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 🤔 输出什么?
}, 1000);
}
// 输出: 3, 3, 3
💡 解决方案
// 方案1: 使用let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000); // 0,1,2
}
// 方案2: IIFE 立即执行函数--》闭包
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 1000); // 0,1,2
})(i);
}
🏆 闭包最佳实践
- 🎯 只在需要时使用 - 不要滥用闭包
- 🧹 及时清理引用 - 避免内存泄漏
- 🚫 避免循环滥用 - 不要在循环中创建不必要闭包
- 📦 模块化组织 - 用闭包实现模块化
延迟执行
setTimeout
和 setInterval
闭包在延迟执行中发挥了重要作用,因为即使外部函数已经执行完毕,闭包仍然可以保持对外部变量的引用