关于javascript:深入理解JavaScript闭包

4次阅读

共计 2638 个字符,预计需要花费 7 分钟才能阅读完成。

在开始讲闭包之前,咱们须要了解作用域和作用域链

作用域链

什么是作用域链?

咱们先看一段代码

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

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

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

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

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

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

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

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

块级作用域的查找

咱们来说说上述的查找过程,当执行到 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)对象中,这时候对象就蕴含了两个变量了。

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

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

正文完
 0