深入浅出JavaScript闭包:你真的懂它吗?
哈喽,各位前端的小伙伴们!今天我们来聊一个让无数面试官"爱不释手"的话题------JavaScript闭包。别看它名字听起来有点"高冷",其实理解起来就像剥洋葱,一层一层,越剥越有味儿!✨
💡 什么是闭包?
闭包,用大白话来说,就是一个"有特权的函数"。它能访问到它"出生"时所在的环境,即使那个环境已经"寿终正寝"了,它依然能"不忘初心",访问到那些变量。是不是有点像你小时候藏在床底下的"私房钱",即使你长大了搬家了,那笔钱还在那里等着你?💰
官方定义: 闭包是指有权访问另一个函数作用域中变量的函数。
最常见的创建方式: 在一个函数内部创建另一个函数。
🔄 闭包的"超能力":两大用途
闭包可不是个"花瓶",它可是有真本事的!主要有两大"超能力":
1. 访问"私密"变量 🔑
闭包的第一个用途,就是让外部世界能够访问到函数内部的"私密"变量。这就像给你的"私房钱"加了一把锁,只有特定的"钥匙"(闭包)才能打开。这样既能保护数据,又能灵活使用。
举个例子:
我们来思考一个简单的计数器。如果不用闭包,你可能会这么写:
javascript
// 初始化计数器
var a = 0;
// 递增计数器的函数
function add() {
a++;
console.log(a);
}
// 调用三次 add()
add(); // 1
add(); // 2
add(); // 3
这个例子中,a
是一个全局变量,任何人都可以修改它,这就不太"安全"了。如果你的同事不小心把 a
改成了 100
,那你的计数器就"乱套"了!
现在,我们用闭包来改造一下:
javascript
function add() {
var a = 0; // a 现在是 add 函数的局部变量,外部无法直接访问
return function adds() { // 这个 adds 函数就是闭包
a++;
console.log(a);
};
}
// 调用三次 add()
const xd = add(); // xd 现在是那个"有特权的函数" adds
xd(); // 1
xd(); // 2
xd(); // 3
看!现在 a
变量被"保护"起来了,外部只能通过 xd()
这个闭包来操作它,是不是安全多了?这就是闭包的魅力所在!
2. 变量"长生不老" ⏳
闭包的第二个用途,就是让那些本该"寿终正寝"的变量,能够"长生不老",继续留在内存中。这就像你把一个重要的文件放在一个"保险箱"里,即使你离开了办公室,那个文件依然在保险箱里,不会被清理掉。
当 add()
函数执行完毕后,它的执行上下文会被销毁,但由于 adds()
(闭包)引用了 add()
作用域中的 a
变量,所以 a
变量不会被垃圾回收机制回收,会一直保存在内存中,直到 adds()
不再被引用。
⚠️ 面试官最爱考的"坑":循环中的闭包问题
说到闭包,就不得不提这个经典的面试题了!多少英雄好汉在这里"折戟沉沙"!
javascript
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
你猜猜这段代码会输出什么?是不是以为会依次输出 1, 2, 3, 4, 5
?
答案是: 会输出五次 6
!😱
为什么呢? 因为 setTimeout
是一个异步函数,它不会立即执行。当 for
循环飞速地跑完之后,i
的值已经变成了 6
。等到 setTimeout
里的 timer
函数真正执行的时候,它去访问 i
,发现 i
已经是 6
了,所以就打印了五次 6
。
🔧 解决方案
别急,解决办法有三种,每种都体现了对闭包的深刻理解!
1. 使用闭包(立即执行函数)
javascript
for (var i = 1; i <= 5; i++) {
(function(j) { // 立即执行函数,创建了一个新的作用域
setTimeout(function timer() {
console.log(j); // 这里的 j 捕获了每次循环的 i 值
}, j * 1000);
})(i); // 每次循环都把当前的 i 值传给 j
}
通过立即执行函数(IIFE),我们为每次循环创建了一个独立的作用域,将当前的 i
值作为参数 j
传递进去。这样,timer
函数引用的就是每次循环独立的 j
,而不是循环结束后的全局 i
。
2. 使用 setTimeout
的第三个参数
这是一个比较"冷门"但很实用的方法!setTimeout
的第三个参数可以直接作为回调函数的参数传入。
javascript
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(i) { // 这里的 i 是 setTimeout 传入的参数
console.log(i);
},
i * 1000,
i // 将当前的 i 值作为第三个参数传入
);
}
这种方法简洁明了,直接利用了 setTimeout
的特性,将每次循环的 i
值"固定"在了 timer
函数的参数中。
3. 使用 let
定义 i
(最推荐的方式)
ES6 引入的 let
关键字完美解决了这个问题!这也是目前最推荐的方式。
javascript
for (let i = 1; i <= 5; i++) { // 使用 let 声明 i
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
let
声明的变量具有块级作用域,每次循环都会创建一个新的 i
。所以,setTimeout
里的 timer
函数会捕获到每次循环独立的 i
值,问题迎刃而解!是不是感觉 let
简直是"救星"?🌟
总结 📝
闭包是JavaScript中一个非常强大且重要的概念。它不仅能帮助我们实现数据封装和私有化,还能解决一些异步编程中的"老大难"问题。理解闭包,就像掌握了一项"魔法",能让你的代码更加灵活、健壮。
希望通过这篇博客,你对闭包有了更深入的理解。下次面试再遇到它,你就可以自信满满地告诉面试官:我,懂闭包!😎
如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、转发哦!👍