深入理解JavaScript闭包

在开始讲闭包之前,我们需要理解作用域和作用域链

作用域链

什么是作用域链?

我们先看一段代码

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

当我们看到这个题目的时候,我们会想到用执行上下文去分析,当执行到bar函数时,调用栈的状态如图:

深入理解JavaScript闭包_第1张图片

上图可以看到有两个myName变量,那bar执行的时候用的是哪一个呢?

其实在每个执行上下文的环境变量中,都包含了一个外部引用(称为outerouterouter),指向外部的执行上下文。

当在bar函数的执行上下文中没有找到myName变量的时候,会通过outer去外部的执行上下文中找这个变量。而bar的outer是直接指向全局执行上下文,然后在全局执行上下中,先在词法环境中从栈顶到栈底查找,如果没有再到变量环境中查找。

有人可能会问:为什么是foo函数调用bar函数,但是outer指向的是全局上下文?

  • 这个其实和词法作用域(静态作用域)有关,简单来说就是代码结构中函数声明的位置来决定,上述的代码中,foo函数和bar函数的上一级作用域是全局作用域,所以如果foo或bar数调用了一个它们没有定义的变量,它们就会到上一级作用域中查找。说白了:词法作用域是代码阶段就决定好了,和函数是怎么调用没有关系

到这里我们已经说明了什么是作用域链了:js沿着词法作用域形成链条一层层往外查找,这个查找的链条就叫做作用域链

块级作用域的查找

深入理解JavaScript闭包_第2张图片

我们来说说上述的查找过程,当执行到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闭包_第3张图片

  • 继续扫面,发现setName函数内部还引用了test1,引擎又将test1添加到closure(foo)对象中,这时候对象就包含了两个变量了。

当执行到foo函数时,闭包就产生了;当foo函数执行结束之后,返回的getName和setName⽅法都用“clourse(foo)”对象,所以即使foo函数退出了,foo函数执行上下文被销毁了,“clourse(foo)”依然被其内部的getName和setNam法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

你可能感兴趣的:(深入理解JavaScript闭包)