闭包(Closure)的定义闭包是JavaScript初级用户既熟悉又陌生的概念。因为我们在写JavaScript代码的时候随处可见闭包,但是我们却不知道闭包用在了哪些地方。关于闭包的定义,网上(书上)的解释总是千奇百怪,我们也只能“取其精华,去其糟粕”来概括。即使在当前作用域外调用函数,它仍然可以访问当前作用域中的变量,并且函数可以访问另一个函数作用域中的变量(函数)。闭包是指可以访问自由变量的函数。在ECMAScript中,闭包是指:从理论上讲:所有的函数都是闭包。因为它们在创建的时候都保存了上层上下文的数据。即使对于简单的全局变量也是如此,因为在函数中访问全局变量等同于访问自由变量,此时使用的是最外层作用域。从实用的角度来看:下面是一个闭包:即使创建它的上下文被销毁,它仍然存在。代码中引用了自由变量。闭包与词法作用域、作用域链和执行上下文相关,这些都是JavaScript中的重要概念。因此,要想真正理解闭包,至少要熟悉那些概念。闭包的优点:可以在函数内部使用变量(函数),也可以说是可以访问函数的作用域。有私有变量,避免污染全局变量闭包缺点:私有变量一直存在,占用内存。下面逐步介绍一下闭包。自执行函数(IIFE)自执行函数也称为立即调用函数(IIFE),即在定义时执行的函数。vara=1;(function(){console.log(a)})()上面的代码是最简单的自执行函数。ES6之前没有块级作用域,只有全局作用域和函数作用域,所以自执行函数在ES6之前也可以实现块级作用域。//ES6块级作用域vara=1;if(true){leta=111;控制台日志(一);//111}console.log(a);//1这里if{}使用let声明一个a。这a具有块级范围。如果在这个{}中访问a,总是会访问let声明的a,与全局作用域中的a无关。如果我们用var替换let,我们将污染全局变量a。如果使用自执行函数实现:vara=1;(function(){if(true){vara=111;console.log(a);//111}})()console.log(a);//1这里为什么引入自执行函数的概念?因为通常我们使用自执行函数来创建闭包来达到一定的效果。让我们看一个基本的面试问题:for(vari=0;i<5;i++){setTimeout(function(){console.log(i);},1000)}在理想状态下,我们期望输出什么是0,1,2,3,4。但实际上输出是5,5,5,5,5。为什么会这样?其实这里不仅仅涉及作用域,作用域链还涉及到EventLoop、microtasks、macrotasks。但我不会在这里谈论这些。下面我们先解释一下为什么输出5个5,然后用一个自执行函数对其进行修改,达到我们预期的效果。提示:在for循环中,每次都声明一个同名变量,下一个变量的值为上次循环执行后同名变量的值。首先,用var声明变量for不会产生块级作用域,所以在()中声明的i是一个全局变量。等同于://伪代码vari;for(i=0;i<5;i++){setTimeout(function(){console.log(i);},1000)}setTimeout中的第一个参数是一个全局匿名函数。等同于://伪代码vari;varf=function(){console.log(i);}for(i=0;i<5;i++){setTimeout(f,1000)}由于setTimeout为1秒后来,此时已经执行了for循环,此时全局变量i已经变成了5。1秒后会同时执行setTimeout中的5个匿名函数,即执行5个f函数。此时f函数使用的变量i根据作用域链的查找规则在全局作用域中找到i。所以输出了五个5。那么我们如何修改呢?思路一:让在setTimeout匿名函数中访问的变量i不再在全局范围内访问i。所以把它包装在一个函数范围内。这时,匿名函数访问变量i时,会先在包裹它的函数范围内查找。for(vari=0;i<5;i++){(function(){setTimeout(function(){console.log(i);},1000)})();}上面的例子会输出我们的值预计?答案是不。为什么?虽然我们把setTimeout包裹在一个匿名函数里面,但是当setTimeout里面的匿名函数执行的时候,先去匿名函数里面找i的值,如果找不到,还是会在全局范围内找,并且最后i的值还是一个全局变量里面的i还是5个5,然后我们在外层匿名函数中声明一个变量j,让setTimeout中的匿名函数访问这个j,这样全局变量中的变量就不能成立。for(vari=0;i<5;i++){(function(){varj=i;setTimeout(function(){console.log(j);},1000)})();}这一次达到了我们预期的结果:01234。让我们对其进行优化:for(vari=0;i<5;i++){(function(i){setTimeout(function(){console.log(i);},1000)})(i);}*思路二:使用let声明变量,生成块级作用域。for(leti=0;i<5;i++){setTimeout(function(){console.log(i);},1000)}此时for循环5次,得到5个块级作用域,即还会声明有5个块级作用域的变量i,所以每次执行setTimeout中的匿名函数时,访问的i都是当前块级作用域的变量i。理论上的闭包什么是理论上的闭包?它看起来像一个闭包,但它不是一个闭包。这就像一个关闭。函数foo(){vara=2;函数栏(){console.log(a);//2}bar();}foo();上面的代码是基于我们在最上面定义的闭包,它并不完全是一个闭包。尽管一个函数可以访问另一个函数中的变量,但嵌套函数是在当前词法范围内调用的。闭包实践我们如何在词法作用域之外执行上述代码的foo函数中的bar函数?下面的代码清楚地显示了闭包:functionfoo(){vara=2;函数栏(){console.log(a);}returnbar;}varbaz=foo();baz();//2-这就是闭包所做的,我的朋友。在上面的代码中,bar被视为foo函数的返回值。foo函数执行后,返回值,也就是bar函数,赋值给全局变量baz。baz执行的时候,其实就是bar函数的执行。我们知道,foo函数执行后,foo的内部作用域会被销毁,因为引擎有一个垃圾回收期,释放不再使用的内存空间。所以当执行bar函数的时候,foo函数里面的作用域其实已经不存在了。应该说是在bar函数内部找不到a变量。但是闭包的魔力就在这里。由于bar是在foo范围内声明的,因此bar函数将始终持有对foo范围的引用。这时候形成了闭包。我们先来看一个例子:varscope="globalscope";functioncheckscope(){varscope="localscope";函数f(){返回范围;}returnf;}varfoo=checkscope();foo();我们用伪代码来解释JavaScript引擎在执行上述代码时的步骤:当JavaScript引擎遇到可执行代码时,会进入一个执行上下文(环境).它首先遇到的是全局代码,因此进入了全局执行上下文。将全局执行上下文推送到执行上下文堆栈上。当创建全局上下文时,VO/AO、作用域链和this将在内部创建。然后执行代码。当一个checkscope函数执行时,它会进入checkscope的执行上下文,然后将其压入执行上下文栈。当创建checkscope执行上下文时,VO/AO、作用域链和this将在内部创建。然后执行代码。当checkscope函数执行时,会从执行上下文栈中弹出,其AO也会被浏览器回收。(这个比较理想)执行foo函数,查找foo的值,发现foo的值是checkscope函数的内部函数f。所以这一步就是执行checkscope的内部函数f。执行f函数与执行checkscope相同。f函数被执行并从执行上下文堆栈中弹出。但是我们思考一个问题。checkscope函数执行后,其执行上下文从栈中弹出,即被销毁,不再存在。f函数仍然可以访问包装函数范围内的变量(范围)吗?答案是肯定的。原因是在第6步中我们说过,checkscope执行函数执行时,会把它的执行上下文从栈中弹出,此时active对象也会被回收。按理说,当f访问不到checkscope的active对象的时候。其实这里还有一个概念,叫做作用域链:checkscope函数创建的时候,会创建对应的作用域链,里面的值存放的是包裹其作用域对应的执行上下文的变量对象,即这里只有全局执行上下文变量对象,checkscope执行时,此时的作用域链发生变化,里面存放的是变量对象(活动对象)的集合,最上面是当前函数执行上下文的活动对象.最低端是全局执行上下文的变量对象。类似:checkscope.scopeChain=[checkscope.AOglobal.VO]当checkscope的执行遇到f函数的创建时,f函数也会创建对应的作用域链,也就是函数执行时对应的作用域链通过默认包装它作为基础。因此,此时创建f函数时的作用域链如下:=[f.AO检查范围。AOglobal.VO]checkscope函数执行时,内部作用域会被回收,但是f函数的作用域链仍然存在,里面存放的是checkscope函数的活动对象,所以当f函数执行时,会从作用域链中移除找到内部使用的作用域标识符,在作用域链的第二个位置找到,即在checkscope.AO中找到变量scope的值。正是因为JavaScript这样做,才有了闭包的概念。也有人说闭包不是为了拥有它而设计的,而是设计作用域链的副作用。闭包是JavaScript中最难的一点,也是面试中经常被问到的问题。我们必须真正了解它。如果只靠死记硬背,是经不起考验的。写在最后,文中如有错误,请务必留言指正,万分感谢。喜欢它,让我们一起学习进步。GitHub
