一篇文章搞懂js中的闭包

写在前面:

闭包可以说是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,嘿嘿

那么要记住了哦

当函数可以记住并访问所在的词法作用域,函数是在当前词法作用域之外执行,这时候就产生了闭包!

最后

到这里博文就结束啦,如果你感觉对你有帮助的话,可以点赞收藏哟,由于笔者能力有限,如果有读者发现了问题,感谢指正!

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页