深入理解JavaScript闭包

在开始讲闭包之前,我们需要理解作用域和作用域链
作用域链
什么是作用域链? 我们先看一段代码

function bar(){ console.log(myName) } function foo(){ var myName='崔斯特' bar() } var myName='卡牌大师' foo()

当我们看到这个题目的时候,我们会想到用执行上下文去分析,当执行到bar函数时,调用栈的状态如图:
深入理解JavaScript闭包
文章图片

上图可以看到有两个myName变量,那bar执行的时候用的是哪一个呢?
其实在每个执行上下文的环境变量中,都包含了一个外部引用(称为outerouterouter),指向外部的执行上下文。
当在bar函数的执行上下文中没有找到myName变量的时候,会通过outer去外部的执行上下文中找这个变量。而bar的outer是直接指向全局执行上下文,然后在全局执行上下中,先在词法环境中从栈顶到栈底查找,如果没有再到变量环境中查找。
有人可能会问:为什么是foo函数调用bar函数,但是outer指向的是全局上下文?
  • 这个其实和词法作用域(静态作用域)有关,简单来说就是代码结构中函数声明的位置来决定,上述的代码中,foo函数和bar函数的上一级作用域是全局作用域,所以如果foo或bar数调用了一个它们没有定义的变量,它们就会到上一级作用域中查找。说白了:词法作用域是代码阶段就决定好了,和函数是怎么调用没有关系
到这里我们已经说明了什么是作用域链了:js沿着词法作用域形成链条一层层往外查找,这个查找的链条就叫做作用域链
块级作用域的查找 深入理解JavaScript闭包
文章图片

我们来说说上述的查找过程,当执行到bar函数的if语句时,因为bar函数的执行上下文中没有定义test变量,根据词法作用域规则,就会到bar函数的外部作用域中查找,也就是全局作用域。在单个执行上下文中的查找规则:先在词法环境中从栈顶到栈底查找,如果没有再到变量环境中查找。
闭包
了解完作用域链之后,我们从作用域链的角度来讲讲什么是闭包!
先看一段代码
function foo(){ var myName = "崔斯特" let test1 = 1 const test2 = 2 var innerBar={ setName:function (newName){ myName = newName }, getName:function (){ console.log(test1) return myName }} return innerBar } var bar = foo() bar.setName("卡牌大师") bar.getName() console.log(bar.getName())

根据词法作用域原则,innerBar中的两个方法可以访问foo函数的两个变量,当inner函数被返回给全局bar变量时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使?foo函数中的变量myName和test1。
看到这里我们可以给闭包下一个定义了!在JavaScript中,根据词法作?域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调??个外部函数返回?个内部函数后,即使该外部函数已经执?结束了,但是内部函数引?外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。?如外部函数是foo,那么这些变量的集合就称为foo函数的闭包。
闭包的回收 当我们闭包使用不正确时,很容易造成内存泄漏
  • 如果引用闭包的函数是一个全局变量,那么这个闭包就会一直存在直到页面关闭,如果这个闭包不再使用的话,就会造成内存泄漏!
  • 如果引用闭包的函数是一个局部变量,等函数销毁后,在下次js引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那垃圾回收器就会回收这块内容。
接下来我们从内存模型来深入理解一下闭包!
我们先来了解一下js在运行的过程中,数据是怎么存储的
在js的执行中有三种内存空间:代码空间、栈空间、堆空间
代码空间是存储可执行代码的,我们主要来看看栈空间和堆空间
栈空间和堆空间
前面讲的调用栈就是我们说的栈空间,存储执行上下文用的,我们来看一段代码:
function foo(){ var a = "崔斯特" var b = a var c = {myName:"崔斯特"} var d = 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(foo)"对象(内部对象,js无法访问)来保存myName。
  • 继续扫面,发现setName函数内部还引用了test1,引擎又将test1添加到closure(foo)对象中,这时候对象就包含了两个变量了。
深入理解JavaScript闭包
文章图片

  • 继续扫面,发现setName函数内部还引用了test1,引擎又将test1添加到closure(foo)对象中,这时候对象就包含了两个变量了。
【深入理解JavaScript闭包】当执行到foo函数时,闭包就产生了;当foo函数执行结束之后,返回的getName和setName?法都用“clourse(foo)”对象,所以即使foo函数退出了,foo函数执行上下文被销毁了,“clourse(foo)”依然被其内部的getName和setNam法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

    推荐阅读