定义
MDN对闭包的定义为:
闭包是指那些能够访问自由变量的函数。
那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
由此,我们可以看出闭包共有两部分组成:
闭包=函数+函数能够访问的自由变量
举个例子:
vara=1;functionfoo(){consol.log(a);}foo();
foo函数可以访问变量a,但是a既不是foo函数的局部变量,也不是foo函数的参数,所以a就是自由变量。
那么,函数foo+foo函数访问的自由变量a不就是构成了一个闭包嘛……
还真是这样的!
所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。
这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:
ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。从实践角度:以下函数才算是闭包:即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)在代码中引用了自由变量
《浏览器原理与实践》给出的定义:
在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
分析
让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动:
varscop="globalscop";functionchckscop(){varscop="localscop";functionf(){turnscop;}turnf;}varfoo=chckscop();foo();
首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。
这里直接给出简要的执行过程:
进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈全局执行上下文初始化执行chckscop函数,创建chckscop函数执行上下文,chckscop执行上下文被压入执行上下文栈chckscop执行上下文初始化,创建变量对象、作用域链、this等chckscop函数执行完毕,chckscop执行上下文从执行上下文栈中弹出执行f函数,创建f函数执行上下文,f执行上下文被压入执行上下文栈f执行上下文初始化,创建变量对象、作用域链、this等f函数执行完毕,f函数上下文从执行上下文栈中弹出
了解到这个过程,我们应该思考一个问题,那就是:
当f函数执行的时候,chckscop函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到chckscop作用域下的scop值呢?
当我们了解了具体的执行过程后,我们知道f执行上下文维护了一个作用域链:
fContxt={Scop:[AO,chckscopContxt.AO,globalContxt.VO],}
对的,就是因为这个作用域链,f函数依然可以读取到chckscopContxt.AO的值,说明当f函数引用了chckscopContxt.AO中的值的时候,即使chckscopContxt被销毁了,但是JavaScript依然会让chckscopContxt.AO活在内存中,f函数依然可以通过f函数的作用域链找到它,正是因为JavaScript做到了这一点,从而实现了闭包这个概念。
闭包怎么回收
通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么JavaScript引擎的垃圾回收器就会回收这块内存。
所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
必刷题
接下来,看这道刷题必刷,面试必考的闭包题:
vardata=[];for(vari=0;i3;i++){data=function(){consol.log(i);};}data[0]();data[1]();data[]();
答案是都是3,让我们分析一下原因:
当执行到data[0]函数之前,此时全局上下文的VO为:
globalContxt={VO:{data:[...],i:3}}
当执行data[0]函数的时候,data[0]函数的作用域链为:
data[0]Contxt={Scop:[AO,globalContxt.VO]}
data[0]Contxt的AO并没有i值,所以会从globalContxt.VO中查找,i为3,所以打印的结果就是3。
data[1]和data[]是一样的道理。
所以让我们改成闭包看看:
vardata=[];for(vari=0;i3;i++){data=(function(i){turnfunction(){consol.log(i);}})(i);}data[0]();data[1]();data[]();
当执行到data[0]函数之前,此时全局上下文的VO为:
globalContxt={VO:{data:[...],i:3}}
跟没改之前一模一样。
当执行data[0]函数的时候,data[0]函数的作用域链发生了改变:
data[0]Contxt={Scop:[AO,匿名函数Contxt.AOglobalContxt.VO]}
匿名函数执行上下文的AO为:
匿名函数Contxt={AO:{argumnts:{0:0,lngth:1},i:0}}
data[0]Contxt的AO并没有i值,所以会沿着作用域链从匿名函数Contxt.AO中查找,这时候就会找i为0,找到了就不会往globalContxt.VO中查找了,即使globalContxt.VO也有i的值(值为3),所以打印的结果就是0。
data[1]和data[]是一样的道理。