在开始讲闭包之前,咱们须要了解作用域和作用域链
作用域链
什么是作用域链?
咱们先看一段代码
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)”。