前言:很多同学对于闭包应该听过很多这样的说法:什么避免变量全局污染;使数据私有化,外部无法修改内部数据;可以让外部可以使用内部的私有数据。以上准确的来说应该算是函数的作用,而且闭包也不止一个函数,那函数有的特性闭包肯定有,单独拿出来说还是比较容易混淆的。
闭包的核心作用就是:使变量可以驻留在内存,不被回收。
一、闭包是什么?——一个背着零食旅行的函数
ps:简易理解:函数里面嵌套函数,且内部函数携带外层函数给的原始作用域(变量,函数,模块等)在其他作用域浪,就会形成闭包。(好像小朋友带着家里钱被坏叔叔骗家里去还被关小黑屋了)
想象你有个爱囤零食的朋友,每次出门都要在背包里塞满饼干。JavaScript的函数就像这位朋友,而**闭包(Closure)**就是它随身携带的"零食背包"。官方说法是:函数和其词法环境的引用捆绑在一起。
举个栗子 :
function 旅行准备() {
const 零食 = "曲奇饼";
return function() {
console.log("我偷偷带了:" + 零食);
};
}
const 间谍函数 = 旅行准备();
间谍函数(); // 输出:我偷偷带了:曲奇饼
这个贪吃鬼函数明明已经执行完毕,却还能在背包(闭包)里保留着零食变量,是不是很像哆啦A梦的四次元口袋?
人话:function函数在该定义的词法作用域之外被执行——盆右,闭包来咯!(只要使用了回调函数就是使用了闭包)
讲道理,旅行准备这个函数在被执行后其整个内部作用域都会被销毁(诶!闭包不让),让我们看看下面的例子。
二、闭包实战手册——那些年我们踩过的坑
案例1:会自己长大的计数器
function 创造计数器() {
let count = 0;
return {
增值: () => ++count,
查账: () => count
};
}
const 我的小金库 = 创造计数器();
我的小金库.增值(); // 1
我的小金库.查账(); // 1
这个计数器就像会下金蛋的鹅,每次调用都在自己的秘密空间里默默记账。
创造计数器函数内部作用域被闭包保留了,count就能被引用。
案例2:定时器谜案(经典面试题)
ps:setTimeout是js引擎内置函数,下面意思就是100毫秒后执行这个函数。
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出3个3!
}, 100);
}
为什么是3个3?这里原理就是当定时器运行时即使每个迭代中执行的时setTimeout(...,0),所有的回调函数依然是在循环结束才会被执行。
这里定时器像三个视力模糊的侦探,全都指向同一个i变量。解决办法是给每个侦探配个相机(闭包):
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0,1,2
}, 100);
}
此处使用了let声明(能劫持块作用域,并在内声明一个变量),上面就是每次迭代都声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
或者用IIFE制造独立取证室:
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout(() => {
console.log(j); // 0,1,2
}, 100);
})(i);
}
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量(上面的“j”)供我们访问。
三、闭包生存法则——不要变成内存刺客
- 内存泄漏风险:闭包就像粘人的小妖精,会一直抓着变量不放。用完记得:
const 我的小金库 = 创造计数器();
// 使用完毕后...
我的小金库 = null; // 解除引用
上面我们讲到闭包的核心作用:使变量可以驻留在内存,不被回收。这既是优点也是缺点,数据长时间驻留在内存不被回收就会内存泄漏。双刃剑,看着用就行。
- 性能优化:避免在循环中创建大量闭包,就像不要在洗衣机里塞满曲奇饼——会糊!
四、总结:闭包是JavaScript的瑞士军刀
- ✅ 优点:实现封装、缓存数据、创建私有空间
- ⚠️ 注意:适度使用,避免内存泄漏
- 🎮 玩法:模块开发、防抖节流、高阶函数
最后送你一句程序员谚语:"不懂闭包的程序员,就像不会系鞋带的跑者——跑不远!" 🏃💨
(本文不承诺曲奇饼不会在闭包中过期,请定期清理内存~)