9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

说明

【浏览器工作原理与实践】专栏学习笔记

例子

先看一个例子

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

其调用栈的状态图如下所示:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
全局执行上下文和 foo 函数的执行上下文中都包含变量 myName,那 bar 函数里面 myName 的值用哪个?

我们先去掉全局变量的一行,去控制台输出一下看看:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
显然说明了 bar 函数里面 myName 的值用的全局变量的,原因是什么?

作用域链

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

带有外部引用的调用栈示意图:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链

我当时看到这儿的时候也有一个问题:那就是那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

要解决这个问题,就需要了解词法作用域因为作用域链是由词法作用域决定的。

词法作用域

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

用一个函数嵌套来表示一下:词法作用域

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。

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

块级作用域中的变量查找

例子:

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()

块级作用域中是如何查找变量的:

首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

闭包

结合代码来理解什么是闭包:

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())

执行结果:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

执行到 return innerBar 时候的调用栈:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

innerBar 对象里包含了 getName 和 setName 的两个方法,方法内部使用了 myName 和 test1 两个变量

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量。

foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这些变量的集合就称为 foo 函数的闭包

闭包的产生过程:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

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

闭包是如何使用的呢?

上面代码执行到 bar.setName 里的 myName = "极客邦" 时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量:如图

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

  1. setName 的执行上下文中没有 myName 变量
  2. foo 函数的闭包中包含了变量 myName
  3. 调用 setName 时,会修改 foo 闭包中的 myName 变量的值

我们通过开发者工具来看看:在代码出加一个 debugger

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
然后展开,开发者工具中的闭包展示如下:

9 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:

  1. Local 就是当前的 getName 函数的作用域
  2. Closure(foo) 是指 foo 函数的闭包
  3. 最下面的 Global 就是指全局作用域

Local–>Closure(foo)–>Global 就是一个完整的作用域链。

闭包是怎么回收的

为什么?

因为如果闭包使用不正确,会很容易造成内存泄漏

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

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

使用闭包原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

思考题

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "极客时间"
    return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()

参考答案:来自网友–《蓝配鸡》

最后输出的都是 “极客邦”,这里不会产生函数闭包。

全局执行上下文:
变量环境:
Bar=undefined
Foo= function
词法环境:
myname = undefined
_printName = undefined

开始执行:
bar ={myname: "time.geekbang.com", printName: function(){...}}

myName = " 极客邦 "
 _printName = foo() 调用foo函数,压执行上下文入调用栈

foo函数执行上下文:
变量环境: 空
词法环境: myName=undefined
开始执行:
myName = " 极客时间 "
return bar.printName
开始查询变量bar, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(没有)-> 查找outer语法环境(找到了)并且返回找到的值
pop foo的执行上下文

_printName = bar.printName
printName()压bar.printName方法的执行上下文入调用栈

bar.printName函数执行上下文:
变量环境: 空
词法环境: 空
开始执行:
console.log(myName)
开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了)
打印" 极客邦 "
pop bar.printName的执行上下文


bar.printName() 压bar.printName方法的执行上下文入调用栈

bar.printName函数执行上下文:
变量环境: 空
词法环境: 空
开始执行:
console.log(myName)
开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了)
打印" 极客邦 "
pop bar.printName的执行上下文
上一篇:Python基础快速上手第一章


下一篇:Java的函数传参对原参数影响--只有容器类和自定义的对象会受影响