在开始讲闭包之前,我们需要了解作用域和作用域链什么是作用域链?我们先来看一段代码说到正题,我们就会想到用执行上下文来分析。执行bar函数时,调用栈的状态如图:上图中可以看到有两个myName变量。执行bar时用的是哪一个?实际上,在每个执行上下文的环境变量中,都包含了一个外部引用(outerouterouter),指向外部执行上下文。当bar函数的执行上下文中没有找到myName变量时,就会去外层执行上下文通过outer查找这个变量。bar的外层直接指向全局执行上下文,然后在全局执行上下文中,先从栈顶到栈底在词法环境中查找,如果没有,再在变量环境中查找.有人可能会问:为什么foo函数调用的是bar函数,outer却指向了全局上下文?这其实和词法作用域(静态作用域)有关。简单的说,就是由函数声明在代码结构中的位置决定的。上面的代码中,foo函数和bar函数的上层作用域是全局作用域,所以如果foo或者bar调用了自己没有定义的变量,就会在上层作用域中查找。说白了:词法作用域是在代码阶段就确定的,与函数的调用方式无关。这里我们已经解释了什么是作用域链:js沿着词法作用域形成一条链,逐层搜索。这个搜索链称为作用域链块级作用域搜索。下面说一下上面的查找过程。bar函数的if语句执行时,由于bar函数的执行上下文中没有定义test变量,根据词法作用域规则,它会在bar函数的外层作用域中查找,也就是全局范围。单个执行上下文中的查找规则:首先在词法环境中从栈顶向栈底查找,如果没有,则在变量环境中查找。闭包了解了作用域链之后,我们再从作用域链的角度来说说什么是闭包!先看一段代码log(test1)returnmyName}}returninnerBar}varbar=foo()bar.setName("CardMaster")bar.getName()console.log(bar.getName())根据词法范围原则,innerBar这两个方法可以访问foo函数的两个变量。当inner函数返回全局bar变量时,虽然foo函数已经执行,getName和setName函数仍然可以使用foo函数中的变量myName和test1。看到这里我们可以定义一个闭包!在JavaScript中,根据词法作用域的规则,一个内部函数总是可以访问在其外部函数中声明的变量。当调用外层函数返回内层函数时,即使外层函数已经执行完毕,但内层函数引用外层函数的变量仍然保存在内存中,我们称这些变量的集合为闭包。例如,如果外部函数是foo,那么这些变量的集合就称为foo函数的闭包。闭包回收当我们错误地使用闭包时,很容易造成内存泄漏。如果引用闭包的函数是一个全局变量,那么闭包将一直存在,直到页面关闭。如果闭包不再使用,就会造成内存泄漏!如果引用闭包的函数是一个局部变量,在函数被销毁后,JS引擎下次执行垃圾回收时,如果判断闭包的内容不再被使用,垃圾回收器就会回收这些内容。接下来,我们就从内存模型来深入了解一下闭包吧!我们先来了解下js运行过程中数据是如何存储的。js的执行中存在三个内存空间:代码空间、栈空间、堆空间。代码空间存储可执行代码。我们主要看栈空间和堆空间栈空间和堆空间上面说的调用栈就是我们所说的栈空间,用来存放执行上下文。我们看一段代码:functionfoo(){vara="Drizzt"varb=avarc={myName:"Drizzt"}vard=c}foo()分析上面代码中变量的存储,a,b赋值的是原始数据类型,所以会压栈执行上下文的变量环境,但是c赋值的是引用类型,这时候情况就不一样了。js引擎会将c和d分配到堆空间。分配后会有堆地址,然后把堆地址赋值给c。可能现在你有一个疑问:把所有的数据都存放在栈空间不好吗?为什么要维护栈空间和堆空间?A。因为js引擎需要使用栈来维护函数的执行上下文,一个函数执行完后,当前函数的执行上下文栈空间会被完全回收,然后js引擎需要离开当前执行上下文,只需将指针向下移动移动到下一个执行上下文就可以了。如果栈空间过大,会影响执行上下文切换的效率,进而影响整个程序的执行效率!b.通常栈空间不会设置的很大,主要是存放一些原始数据类型。堆空间比较大,适合存放一些引用类型的数据,占用空间比较大。从内存模型的角度来看闭包,见上面的例子。当foo函数的执行上下文被销毁时,由于foo函数产生了一个闭包,变量myName和test1并没有被销毁,而是保存在了内存中。这个过程在内存中是什么样子的?当js执行foo函数时,会先编译。在编译过程中,遇到了内部函数setName。js引擎会对内部函数进行词法扫描,发现内部函数引用了foo函数中的myName变量。js引擎会判断这是一个Closure,于是会在堆空间创建一个“closure(foo)”对象(内部对象,js无法访问)来保存myName。继续扫描,发现setName函数内部也引用了test1,引擎将test1添加到closure(foo)对象中。此时,该对象包含两个变量。foo函数执行时,产生闭包;foo函数执行时,返回的getName和setName方法都使用了“clourse(foo)”对象,所以即使foo函数退出,foo函数执行上下文被销毁了,现在仍然引用“clourse(foo)”通过其内部的getName和setNam方法。因此,下次调用bar.setName或bar.getName时,创建的执行上下文将包含“clourse(foo)”。
