作用域

对于大多数编程语言来说,变量是最基本的功能,正是变量带给程序状态。在 JavaScript 中,如何存储变量以及程序之后如何查找调用的机制被称为作用域。

通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

分层

作用域是分层的,内部函数可以访问外部函数作用域的变量,反之则不行。

块级作用域

JavaScript 设计简单的一点在于没有块级作用域,变量靠提升能在代码执行前获取到,无论它在哪里被声明。但提升造成的问题也成了 JavaScript 被人诟病的一点。

ES6 弥补了早先 JavaScript 设计缺陷,提出了块级作用域的概念。这样就和 Java、C/C++类似了。

本质

JavaScript 引擎是通过变量环境实现函数级作用域的,那么 ES6 又是如何在函数级作用域的基础之上,实现对块级作用域的支持呢?

// 内部是怎么查找的
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);
}

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

TDZ

let myname = "13pro";
{
  console.log(myname);
  let myname = "Lex";
}

根据词法分析,应该打印13pro,实际上输出

ReferenceError

ReferenceError: Cannot access 'myname' before initialization

报错提示说明变量查找成功,但是不能在初始化之前访问。可以说let仍然会提升变量声明,只不过具有 TDZ(暂停性死区)限制。这有利于我们对代码进行词法分析,不然块作用域中的声明放哪呢?

小结

  • 变量提升通过变量环境来实现
  • 块级作用域通过词法环境的栈结构来实现
  • let、const 声明变量具有 TDZ 限制
  • 语言的设计大都类似,参考 JavaScript 协程和 JavaScript 中的虚拟机实现机制