一段代码彻底搞懂作用域链和闭包

502 阅读3分钟

块级作用域及作用域链

一段代码搞清楚 js 里的执行上下文和调用栈 中我们理解了 js 里的执行上下文以及调用栈的形成。

现在再来看一个题目:

function bar() {
    var myName = "xiaoyu"
    let test1 = 100
    if (1) {
        let myName = "zhenyu"
        console.log(test)
    }
}
function foo() {
    var myName = "zhongyu"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "baoyu"
let myAge = 10
let test = 1
foo()

当 bar 中执行到 console.log(test) 时,js 调用栈如下:

js 代码编译阶段,全局代码中,如果遇到了 let 或者 const 声明的变量,会把变量放到全局执行上下文的词法环境(Lexical Environment)中。

函数作用域中声明的 let 或者 const 变量,会在函数执行上下文创建后,放在函数的执行上下文的词法环境中。词法环境中的变量以栈的形式存放,栈底是函数内最外层的变量,当一个块级作用域执行完成后,对应的信息会从栈顶弹出。

这是 let、const 和 var 的不同之处。

我们已经知道,var 存放在变量环境中。变量环境中除了变量对象外,还有一个指向外部执行上下文的 outer ,这个 outer 与代码怎么执行无关,而是在代码编写的时候确定,也就是,与静态的词法作用域有关。

js 引擎在当前作用域找不到变量时,会沿着 outer 的指向去外部查找变量,这个查找的链条就是作用域链。

有块级作用域的参与时, js 变量的查找顺序如下:

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

单个的执行上下文中,首先查找词法环境,沿着栈从顶向下查询,查到了就返回,没查到继续在变量环境中找。

如图所示:

因此会打印出来 1 。

闭包是怎么一回事

function foo() {
    var myName = "xiaoyu"
    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("baoyu")
bar.getName()
console.log(bar.getName())

用上边的方法分析,return innerBar 之前,调用栈的情况如下:

setName 和 getName 都是在 foo 内部定义的,并且都使用了 myName 和 test1 两个变量。

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

return innerBar 执行之后,调用栈中情况如下:

因为 myName 和 test1 还要被 setName 和 getName 使用,因此 myName 和 test1 已然保存在内存中。这个包含了 myName 和 test1 的变量集合,可以被叫做闭包:

Chrome 开发者工具里,Scope 中的 Local -> Closure -> Global 就是一个完整的作用域链。

那么,闭包上下文里的变量怎么用呢?bar.setName 函数执行 myName = newName 时,myName 的查找顺序是: 当前执行上下文-> foo 闭包 -> 全局执行上下文,调用栈如图:

bar.getName 访问 myName 也是同样的顺序。

如果引用闭包的函数是一个全局变量,闭包会一直存在直到页面关闭,因此会造成内存泄漏。