❓ 闭包到底是什么?
闭包 = 内部函数 + 它绑定的外部变量环境
既不是外部函数,也不是返回的对象,而是「内部函数和它锁定的外部变量」的组合体!
❓ 为什么叫「闭包」(Closure)?
- 词源:来自拉丁语「clausura」(关闭、封闭),意为「封闭了外部变量的函数」。
- 核心逻辑:内部函数像一扇门,把外部变量「关」在自己的作用域里,形成封闭的包裹体。
- 哲学意义:函数不再只是孤立的代码块,而是携带了「记忆」的独立个体!
🌰 例子:会嘲讽的ATM机
function 创建ATM机(初始余额) {
let 余额 = 初始余额; // 闭包藏起来的钱!
return {
取钱: (金额) => {
if (金额 > 余额) console.log("😶余额不足,赶紧去赚钱!");
else {
余额 -= 金额;
console.log(`取出${金额},余额:${余额}`);
}
},
存钱: (金额) => {
余额 += 金额;
console.log(`存入${金额},余额:${余额}`);
}
};
}
const 我的ATM = 创建ATM机(100);
我的ATM.取钱(50); // 余额50
我的ATM.存钱(200); // 余额250
我的ATM.取钱(300); // 余额不足,开始嘲讽
🔍 解剖闭包身份:
| 角色 | 对应代码 | 是否是闭包? |
|---|---|---|
外部函数 创建ATM | function 创建ATM机(){...} | ❌ 只是闭包的「诞生工厂」 |
内部函数 取钱/存钱 | () => { ...余额 } | ✅ 闭包本体(函数+环境) |
| 返回的对象 | { 取钱, 存钱 } | ❌ 对象的属性是闭包 |
💡 关键理解:
- 闭包是「内部函数」的超级形态:普通函数执行完就失忆,闭包函数却能记住外部变量。
- 环境是闭包的一部分:闭包函数不能脱离它引用的变量。
- 命名精髓:闭包 = 封闭(closure)了外部变量,形成一个自给自足的包裹体。
🌰 再举个例子:
// 外部函数只是「造物主」
function 创建计数器() {
let count = 0; // 被闭包关起来的变量
// 闭包:内部函数 + count
return () => {
count++;
console.log(`计数:${count}`);
};
}
const 计数器 = 创建计数器();
计数器(); // 计数:1
计数器(); // 计数:2 (普通函数早该失忆了,闭包却记得!)
🎯 闭包常见使用场景与示例
1. 模块化与私有变量
场景:隐藏内部变量,只暴露特定方法(类似「保险箱」)。
// 创建一个计数器模块,count 是私有变量
function createCounter() {
let count = 0; // 闭包保护的变量
return {
increment: () => { count++; },
getValue: () => count,
reset: () => { count = 0; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
counter.reset();
// 无法直接访问 counter.count,闭包保护了隐私!
2. 函数工厂(定制化函数)
场景:根据参数生成不同功能的函数(类似「流水线生产」)。
// 创建不同倍数的乘法器
function createMultiplier(factor) {
return (num) => num * factor; // 闭包记住了 factor
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10(5*2)
console.log(triple(5)); // 15(5*3)
3. 事件处理(保留上下文)
场景:在异步操作中保持变量状态
// 按钮点击计数器(闭包保存点击次数)
function setupButton() {
let clicks = 0;
document.getElementById('myBtn').addEventListener('click', () => {
clicks++;
console.log(`点击次数:${clicks}`);
});
}
setupButton();
// 每次点击都记住 clicks,无需全局变量!
4. 缓存(记忆化优化)
场景:缓存复杂计算结果,避免重复计算
// 缓存阶乘计算结果
function createFactorial() {
const cache = {}; // 闭包中的缓存对象
return (n) => {
if (n in cache) return cache[n];
if (n === 1) return 1;
const result = n * factorial(n - 1);
cache[n] = result; // 存入缓存
return result;
};
}
const factorial = createFactorial();
console.log(factorial(5)); // 120(首次计算)
console.log(factorial(5)); // 120(直接从缓存读取)
5. 延迟调用(setTimeout)
场景:在异步回调中保留循环变量(解决经典面试题)。
// 错误写法:所有回调都输出 i=5
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// 正确写法:闭包保存每个 i 的值
for (var i = 0; i < 5; i++) {
(function (currentI) { // 闭包立即捕获 currentI
setTimeout(() => console.log(currentI), 100);
})(i); // 传递当前 i
}
// 输出 0,1,2,3,4
🌟 闭包诞生的基石与内存逻辑:
- 词法作用域(静态作用域): 函数在定义时确定作用域链,而非运行时!
- 垃圾回收(GC)机制:当内存空间不再被引用时,会被自动回收。
闭包通过「作用域链」强行维持外部变量的引用 → 阻止GC回收!
🚨 注意事项:
- 内存泄漏:长期持有的闭包可能占用内存,用完后及时解除引用(如设为
null)。 - 不要滥用:简单场景用闭包反而让代码难读,优先考虑函数参数传递。
- 引擎优化:即使闭包存在,如果变量后续未被使用,可能被优化回收(如V8引擎的「逃逸分析」)。