10-作用域链和闭包

126 阅读5分钟

作用域链和闭包

我们先来看一段代码

 function bar() {
     console.log(myName)
 }
 function foo() {
     var myName = " 极客邦 "
     bar()
 }
 var myName = " 极客时间 "
 foo()

根据我们之前学过的知识,现在我们可以通过上下文来分析代码的执行流程了,其调用栈的状态图如下:

作用域链例子.png

现在全局执行上下文和foo函数的执行上下文中都有变量myName,那么在打印的时候我们应该优先选择哪个?

如果按照上一节词法环境的查找,会从栈顶开始查找,如果我们的变量环境也按照这种方式查找,那么打印出来的就应该是极客邦

但是事实并非如此,打印出来的应该是极客时间

作用域链

其实每个执行上下文的变量环境中,都包含一个外部引用,用来指向外部的执行上下文,把这个外部引用称为**outer**

所以现在查找一个变量,会先在当前的执行上下文中查找该变量如果没有找到则查找outer所指向的执行上下文,如图所示(这里不考虑let的情况)

变量环境的外部引用.png

这里可以看到barfoo的外部变量指向的都是全局执行上下文,那为什么是指向全局执行上下文呢,这里就要了解词法作用域

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预判代码在执行过程中如何查找标识符

词法作用域.png

可以看到,词法作用域链为:foo函数作用域 -> bar函数作用域 -> main函数作用域 -> 全局作用域

所以很自然,前面的foobar函数的上级作用域是全局作用域,所以外部变量指向的应该是全局作用域

词法作用域是代码阶段就决定好了,和函数怎么调用并没有关系

块级作用域中的变量查找

这里再看一段代码:

 function bar() {
     var myName = " 极客世界 "
     let test1 = 100
     if (1) {
         let myName = "Chrome 浏览器 "
         console.log(test)
     }
 }
 function foo() {
     var myName = " 极客邦 "
     let test = 2
     {
         let test = 3
         bar()
     }
 }
 var myName = " 极客时间 "
 let myAge = 10
 let test = 1
 foo()

分析这一段代码的输出:

这是函数执行到打印处的调用栈状态,现在我们就可以开始查找变量了,先在词法环境中自顶向下查找,找不到就去本函数的上下文中的变量环境查找再找不到就去该函数的outer外部变量指向的作用域中找,也是由词法环境找到变量环境,也就是按照上图的1、2、3、4、5的顺序查找

闭包

我们先来看一段代码:

 function foo() {
     var myName = " 极客时间 "
     let test1 = 1
     const test2 = 2
     var innerBar = {
         getName:function(){
             console.log(test1)
             return myName
         },
         setName:function(newName){
             myName = newName
         }
     }
     return innerBar
 }
 var bar = foo()
 bar.setName(" 极客邦 ")
 bar.getName()
 console.log(bar.getName())

根据调用栈的相关知识,我们可以看到,当代码执行到return innerBar的时候调用栈的情况:

闭包例子调用栈1.png

根据词法作用域的规则,内部函数getNamesetName可以访问外部函数foo中的变量,所以innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是上面两个内部函数还是可以使用foo函数中的变量myNametest1,所以foo函数执行完后,整个调用栈状态如下:

闭包例子调用栈2.png

foo执行完后,其执行上下文会从栈顶弹出,但是由于返回了innerBar其中包含着两个内部函数,还使用了foo函数中的变量myNametest1,所以这个变量依然保持再内存中,所以这就像是一个foo函数的专属背包

由于只有两个内部函数能访问该背包,其他地方无法访问,所以我们就把这个背包称为闭包

所以现在我们可以给闭包一个定义:在JS中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量集合称为闭包

知道闭包的定义之后,我们再来继续看一下上述代码的执行过程,现在执行到bar.setName(" 极客邦 "),现在调用栈的状态应该变成:

闭包例子调用栈3.png

现在要查找myName变量,就会先从setName函数中查找,如果没有就会查找闭包中的myName变量,找到后会修改闭包中的myName变量的值

闭包回收

如果引用闭包的函数是一个全局变量,那么闭包会一直存在到页面关闭,但如果这个闭包以后不会使用的话,就会造成内存泄漏

如果引用闭包的函数是一个局部变量,那么函数销毁后,下次JS引擎执行垃圾回收时,如果闭包这块内容不用再被使用的话,那么JS的垃圾回收机制就会回收这块内存

所以使用闭包要注意一个原则:如果闭包会一直使用,那么就让他作为全局变量存在,如果使用频率不高,内存占用又比较大的时候,就尽量让其成为一个局部变量