块级作用域:var缺陷以及为什么要引入let和const?

118 阅读3分钟

“探病因”——分析为什么在 JavaScript 中会存在变量提升,以及变量提升所带来的问题缺陷;然后“开药方”——介绍如何通过块级作用域并配合 let 和 const 关键字来修复这种缺陷

变量提升所带来的问题:

  1. 变量容易在不被察觉的情况下被覆盖掉;
  2. 本应销毁的变量没有被销毁;
function foo(){
    var a = 1
    let b = 2
    {
        let b = 3
        var c = 4
        let d = 5
        console.log(a)
        console.log(b)
    }
    console.log(b)
    console.log(c)
    console.log(d)
}
foo()

执行流程: 第一步是编译并创建执行上下文

image.png

函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中。

当执行到代码块里面时,变量环境中 a 的值已经被设置成 了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就如下图所示:

image.png

从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。

在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过 let 或 者 const 声明的变量。

再接下来,当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变 量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

image.png

当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文。作用域执行完成示意图: image.png

想必你已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合, JavaScript 引擎也就同时支持了变量提升和块级作用域了。

总结:由于 JavaScript 的变量提升存在着变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。

案例: image.png

在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区

Cannot access 'myname' before initialization 虽然该变量已经在词法环境中了,但是还没有被赋值,所以不能使用! 这也是JavaScript 语法层面的标准,JavaScript引擎是按照标准来实现的。