写在前面:
闭包可以说是js中一个非常难理解得问题了,理解闭包得前提是深入理解作用域得概念,如果你不是很清楚得话,可以看一下我上一篇文章 深入理解作用域
废话不多说,切入正题
如何理解闭包
如果你还没有真正理解闭包得话,那么理解闭包可以看作某种意义上得重生 - - -《你不知道得js》
js中闭包无处不在,它并不是一个需要学习新的语法或模式才能使用得工具,闭包是基于词法作用域书写代码时所产生得自然结果。我们需要得是根据自己得意愿来识别、拥抱和影响闭包得思维环境。
我的理解定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即:使函数是在当前词法作用域之外执行
分析这段代码,思考是闭包吗?
function foo(){
var a=2;
function bar(){
console.log(a); //2
}
bar();
}
foo();
这段代码看起来和嵌套作用域中的示例代码很相似,基于词法作用域的查找规则,函数bar()可以访问外部作用域中的变量a,从技术上看也许是闭包,但根据前面的定义,我觉得并不是闭包!最准确的用来解释bar()对a的引用的方法是词法作用域的查找规则,这些规则是闭包中非常重要的一部分!
但是从纯学术上看,可以认为bar()封闭在foo()的作用域中。也可以认为是闭包,但是这种方式定义的闭包并不能直接进行观察,也不能明白在这个代码中闭包是如何进行工作的。所以可以换一种清晰的表达方式!
下面这段代码则清晰的展示了闭包
function foo(){
var a=2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo()
baz(); //2
这就很清晰的可以看出来是一个闭包了!
函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。 foo()执行后,其返回值赋值给baz并调用baz(),实际上只是通过不同的标识符引用调动了内部的函数bar()。当foo()执行后,通常会期待foo()函数的内部作用域被销毁,但是闭包的神奇之处就是可以阻止垃圾回收器对它的回收,事实上内部作用域依然存在,因此没有被回收,因为bar()仍然在使用这个内部作用域!
拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。bar()仍然持有对该作用域的引用,而这个引用就叫做闭包!!
函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。当然,无论使用什么方式对函数类型的值进行传递,当函数在其他地方被调用的时候都可以观察到闭包!
function foo(){
var a=2;
function baz(){
console.log(a); //2
}
bar(baz);
}
function bar(fn){
fn();
}
foo();
把内部函数baz传递给bar,当调用这个内部函数时(即fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它可以访问a。
间接传递函数
var fn;
function foo(){
var a=2;
function baz(){
console.log(a);
}
fn = baz; //将baz分配给全局变量
}
function bar(){
fn();
}
foo();
bar(); //2
无论通过什么手段将内部函数传递到所在的词法作用域以外,它都会保持对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
我们再来看一个示例
function wait(message){
steTimeout(function time(){
console.log(message);
},1000)
}
wait("hello ,米老鼠");
将一个内部函数(time)传递给setTimeout()。time具有涵盖wait()作用域的闭包,保持有对message的引用!
wait()执行1秒后,它的内部作用域并不会消失,time函数依然保有wait()作用域的闭包。词法作用域保持完整,不会被销毁!
这就是闭包!
闭包之经典循环
目的:我们想要这段代码分别输出数字1-5,每秒一次,每次一个! for(var i=1;i<=5;i++){
setTimeout(function time(){
console.log(i);
},i*1000)
}
这段代码你可能遇到过,笔者在做一些公司的笔试的时候经常看到这个题,有时候面试官也会考察这个题,如果你对闭包理解透彻的话,那么这段代码的输出结果你应该非常清楚了吧?
这段代码会输出什么呢?输出1 2 3 4 5?
事实上,这段代码会输出5次6,惊不惊喜?意不意外?
为什么?
- 先解释下6怎么来的昂,跳出for循环条件是i不再小于等于5,所以当i为6时不再进行循环,输出显示的是循环结束的最终值!
小朋友,你是否有很多问号?
仔细想一想,延迟函数的回调会在循环结束的时候才执行,。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(…,0),所有的回调函数仍然是在循环结束之后才会被执行,因此每次都输出6
那到底是什么缺陷导致这样呢????
我们试图假设循环中的每个迭代在运行的时候都会给自己捕获一个i的脚本,但是根据作用域的原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但他们都是被封闭在一个共享的全局作用域中,实际上只有一个i
所以缺陷是什么?我们需要更多的闭包作用域,特别是在循环过程中每个迭代都需要一个闭包作用域!
那么怎么改成是我们想要的效果呢???
我们知道IIFE会通过声明并立即执行一个函数来创建作用域。
那么有了,我写了下面这个代码看看能实现预期效果吗?
for(var i=1;i<=5;i++){
(function(){
setTimeout(function time(){
console.log(i);
},i*1000)
})();
}
这样可以吗?不可以!!!
现在显然拥有更多的词法作用域了,的确每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来,但是如果作用域是空的,那么只封闭自然是不行的,仔细看一下上述代码,IIFE是一个空作用域!!它需要拥有自己的变量,用来存储每个迭代中i的值!
所以我们修改一下上述代码,就可以实现预期效果啦
for(var i=1;i<=5;i++){
(function(){
var j=i;
setTimeout(function time(){
console.log(j);
},j*1000)
})()
}
这个时候,它就会输出1 2 3 4 5啦!
女朋友觉得上述代码不够优雅?
嘿嘿,可以修改一下写法
for(var i=1;i<=5;i++){
(function(j){
setTimeout(function time(){
console.log(j);
},j*1000)
})(i)
}
这样看起来是不是舒服多啦!
那么思考一下,还可以怎么修改呢?
利用块作用域,也可以实现(ES6新增了一个let,可以利用let实现)
如果不了解块作用域,词法作用域可以去看一看哟 理解块作用域,词法作用域,如果let 的使用不清楚的话,可以去看看我的这篇博文哟理解var let const的区别
修改之后的代码如下:
for(var i=1;i<=5;i++){
let j=i;
setTimeout(function time(){
console.log(j);
},j*1000)
}
这样就可以了,是不是很简单?
还有一个更简单的,如果你理解了var 和let的区别,那么我们可以直接这样写:
for(let i=1;i<=5;i++){
setTimeout(function time(){
console.log(i);
},i*1000)
}
怎么样,是不是觉得块作用域和闭包联合使用超级舒服?我不要你觉得,反正我觉得很nice,嘿嘿
那么要记住了哦
当函数可以记住并访问所在的词法作用域,函数是在当前词法作用域之外执行,这时候就产生了闭包!
最后
到这里博文就结束啦,如果你感觉对你有帮助的话,可以点赞收藏哟,由于笔者能力有限,如果有读者发现了问题,感谢指正!
转载:https://blog.csdn.net/weixin_42878211/article/details/105707527