JavaScript块级作用域

背景知识

缺陷并存

从变量提升的文章中,可以感受到JS中行为很反直觉,这也是JS的一种设计缺陷。 ECMAScript6(简称ES6)通过引入块级作用域并配合 const、let关键字来绕开这个缺陷。

由于JS需要兼容以前的浏览器所以很多问题都会继续保持。缺陷和改进方案是一个并存的状态。

作用域(Scope)

为什么JS存在变量提升,其他语言不常见。这得从作用域讲起。

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗的理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

ES6之前,作用域只有两种:全局作用域、函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

  • 函数作用域就是函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问,函数执行结束之后,函数内部定义的变量会被销毁。

相较而言,其他语言普遍支持会计作用域。块级作用域就是使用一对大括号包裹着一段代码比如函数、判断语句、循环语句甚至单独的一个{}都可以被看作是一个块级作用域。

ES6之前是不支持块级作用域的,因为JS作者没想到JS会火,所以只按照最简单的方式来设计。

没有块级作用域,再把作用域内部的变量统一提升,无疑是最简单最快速的设计。不过这样导致了函数中的变量不论声明在那里都可以被访问到,这就是JS中的变量提升。

变量提升带来的问题

由于变量提升,使用JS编写和其他语言相同逻辑的代码,有可能产生不一样的结果。主要在于:

  1. 变量容易在不被察觉的情况下被覆盖调

  2. 本应该销毁的变量没有被销毁

function foo() {
    for(var i=0;i<10;i++){
        
    }

    console.log(i)
}

foo()

这段就和C语言或者其他大部分语言执行结果不同。

ES6如何解决变量提升带来的问题

ES6引入了let、const关键字,是的JS同样拥有了会计作用域。

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

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

词法作用域是一个小型的栈结构,栈底是函数最外层的变量,进入一个作用域块后(可能是内部的 {} ) 就会把该作用域快内部的变量压倒栈顶;当作用域块执行完成之后,作用域块的信息就会从栈顶弹出,这就是词法作用域的结构。这里的变量是 let、const声明的。

------ foo 执行上下文 --------------------------
| ----------------
| | 变量环境       |
| | a = undefined |
| | c = undefined |
| |----------------
|
|  ------------------------------------------
|  |词法环境 (栈结构)                         |
|  |                                        |
|  |                                        |
|  | [b=undefined, d= undefined]            |
|  | [ b= undefined]                        |
|   ----------------------------------------
|-------------------------------------------

如果执行到 console.log(a)这行代码,就需要在词法环境中查找变量a,具体的查找方法:

沿着词法环境的栈顶向下查询,如果在此法环境中某个块找到了,就返回给JS引擎,如果没有找到,继续在变量环境在查找。

Lexical_Environment_find_var.png

当作用域块执行结束之后,内部定义的变量就会从词法作用域栈顶弹出。

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

暂时性死去

执行上下文是编译时创建的,在代码执行的时候已经有词法作用域了,而且变量默认的值是undefined。

暂时性死去是一个语法规定。也就是说 let\const声明的变量已经在词法环境中,但是在没有赋值之前,访问改变量JS就会抛出一个错误。

参考

Mark24

Everything can Mix.