作用域
对于大多数编程语言来说,变量是最基本的功能,正是变量带给程序状态。在 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 中的虚拟机实现机制