🎒 JavaScript 闭包:函数背后的"背包"
🤔 什么是闭包?
官方定义 :
闭包是指有权访问另一个函数作用域中变量的函数。
通俗解释 :
想象一个函数是一个人 ,他出生时背了一个背包 (Scope/作用域)。
这个背包里装着他出生时周围环境中的所有变量。
即使这个人走到了世界的尽头(全局环境),或者他的父母(外部函数)已经去世(执行完毕),他依然背着那个背包,可以随时拿出里面的东西使用。
一句话总结 :
闭包 = 函数 + 函数能访问到的自由变量(背包里的东西)。
📂 目录
- [🎒 核心原理:为什么会有闭包?](#🎒 核心原理:为什么会有闭包?)
- [🛠️ 闭包的三大核心作用](#🛠️ 闭包的三大核心作用)
- [💻 代码实战:经典案例解析](#💻 代码实战:经典案例解析)
- [⚠️ 双刃剑:内存泄漏与性能](#⚠️ 双刃剑:内存泄漏与性能)
- [💡 总结](#💡 总结)
1. 🎒 核心原理:为什么会有闭包?
要理解闭包,首先要理解 JS 的词法作用域(Lexical Scoping)和垃圾回收机制。
正常情况:函数执行完,变量销毁
javascript
function outer() {
let a = 10;
console.log(a);
}
outer(); // 10
// 函数执行完毕,a 被垃圾回收器回收,内存释放。
特殊情况:内部函数引用了外部变量
javascript
function outer() {
let a = 10;
function inner() {
console.log(a); // inner 引用了 outer 的变量 a
}
return inner; // 将 inner 返回出去
}
const myFunc = outer();
myFunc(); // 10
发生了什么?
outer执行完毕,按理说a应该被销毁。- 但是,
inner函数被返回并赋值给了myFunc。 inner的定义中包含了对a的引用。- JS 引擎发现:"嘿,
inner还在外面被人拿着呢,它还要用a,所以我不能销毁a!" - 于是,
a所在的内存空间被保留下来,形成了闭包。
2. 🛠️ 闭包的三大核心作用
闭包不仅仅是理论,它在实际开发中有三个非常强大的用途:
✅ 作用一:数据封装与私有变量(模拟私有属性)
在 ES6 Class 出现之前,JS 没有真正的"私有变量"。闭包是实现模块化和数据隐藏的核心手段。
场景 :创建一个计数器,外部只能调用 increment,无法直接修改 count。
javascript
function createCounter() {
let count = 0; // 私有变量,外部无法直接访问
return {
increment: function () {
count++;
return count;
},
decrement: function () {
count--;
return count;
},
getCount: function () {
return count;
},
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.count); // undefined ✅ 无法直接访问
价值:保护数据不被意外篡改,提供清晰的 API 接口。
✅ 作用二:函数柯里化与参数复用
闭包可以"记住"之前传入的参数,从而实现函数的部分应用(Partial Application)。
场景:固定税率计算。
javascript
function makeTaxCalculator(taxRate) {
// taxRate 被闭包"记住"了
return function (price) {
return price * taxRate;
};
}
const calcVAT = makeTaxCalculator(0.13); // 增值税率 13%
const calcIncomeTax = makeTaxCalculator(0.2); // 所得税率 20%
console.log(calcVAT(100)); // 13
console.log(calcIncomeTax(100)); // 20
价值:减少重复传参,提高代码复用性。
✅ 作用三:异步回调中的状态保持
在异步编程(如定时器、AJAX、事件监听)中,闭包确保回调函数执行时,依然能访问到定义时的上下文变量。
场景:延迟打印日志。
javascript
function logAfterDelay(msg, delay) {
setTimeout(function () {
// 这里的 msg 和 delay 即使在 setTimeout 触发时(几秒后)依然可用
console.log(`${msg} after ${delay}ms`);
}, delay);
}
logAfterDelay("Hello", 1000);
3. 💻 代码实战:经典面试题解析
❌ 经典陷阱:循环中的闭包
这是面试中最常考的闭包问题。
javascript
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
预期输出 :1, 2, 3, 4, 5
实际输出:6, 6, 6, 6, 6 😱
原因:
var没有块级作用域,所有定时器共享同一个全局变量i。- 当定时器执行时,循环已经结束,
i变成了 6。
✅ 解决方案 1:使用 IIFE(立即执行函数)创建闭包
javascript
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j); // j 是每次循环独立的副本
}, j * 1000);
})(i);
}
原理 :IIFE 为每次循环创建了一个新的作用域,将当前的 i 值作为参数 j 传入并保存。
✅ 解决方案 2:使用 let(推荐)
javascript
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
原理 :let 具有块级作用域,JS 引擎会为每次循环迭代创建一个新的绑定。
4. ⚠️ 双刃剑:内存泄漏与性能
闭包虽然强大,但滥用会导致内存泄漏。
📉 为什么会导致内存泄漏?
正常情况下,函数执行完后,局部变量会被回收。
但如果形成了闭包,且外部一直持有对该闭包函数的引用,那么闭包所引用的所有外部变量都不会被回收。
危险示例:
javascript
function hugeDataHandler() {
const hugeArray = new Array(1000000).fill("data"); // 占用大量内存
return function () {
console.log(hugeArray.length); // 只要这个函数存在,hugeArray 就不会被回收
};
}
const handler = hugeDataHandler();
// 如果你不再需要 handler,必须手动解除引用
handler = null; // ✅ 允许 GC 回收 hugeArray
🛡️ 最佳实践
- 及时解除引用 :如果不再需要闭包函数,将其赋值为
null。 - 避免在闭包中引用巨大的无用对象。
- 谨慎使用全局闭包:尽量缩小闭包的作用范围。
💡 总结
| 特性 | 说明 |
|---|---|
| 本质 | 函数 + 引用的自由变量 |
| 核心能力 | 延长变量生命周期,实现数据私有化 |
| 主要用途 | 1. 封装私有变量 2. 柯里化/参数复用 3. 异步回调状态保持 |
| 副作用 | 可能导致内存泄漏,增加内存占用 |
| 解决循环陷阱 | 使用 let 或 IIFE |
🚀 博主寄语 :
闭包不是魔法,它是 JS 作用域机制的自然产物。
不要为了用闭包而用闭包 。当你需要隐藏数据 、记住状态 或复用逻辑时,闭包就是你的最佳伙伴。
记住口诀 :
函数嵌套生闭包,
外部变量怀里抱。
私有数据能封装,
异步回调少不了。
用完记得清引用,
内存泄漏要防好。
希望这篇文档能帮你彻底搞懂闭包的作用!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️