作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
在 ES6 之前,作用域分为两种:
- 全局作用域中的对象在代码中的任何地方都可以访问,其生命周期伴随着页面的生命周期(var定义)
- 函数作用域是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
相较而言,其他语言则普遍支持块级作用域。块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至一个单独的{}都可以被看作是一个块级作用域(注意,对象声明中的{}不是块级作用域)。块级作用域内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。
ES6 之前是不支持块级作用域的,没有块级作用域,将作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。
变量提升可能会引起混乱,比如下面的代码
function varTest() {
var x = 1;
if (true) {
var x = 2;
console.log(x); // 2
}
console.log(x); // 2
}
那么const 和 let 是如何做到的呢?
我们需要知道几件事:
- 函数内部通过var声明的变量,在编译阶段全都被存放到
变量环境里面 - 函数下非作用域块声明的let/const变量,在
编译阶段会被存放到词法环境中 - 函数的作用域块内部声明的let/const变量,编译阶段并没有被存放到词法环境中,
执行到代码块时才会存放到该函数的词法环境中
简单例子
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()
1.编译并创建foo函数执行上下文,根据第一条原则,a c会被放到变量环境中;根据第二条原则,b会放到词法环境中;根据第三条原则,剩下变量暂不处理
2.执行代码,变量环境中a的值变为1,词法环境中b的值变为2
3.执行代码,执行到代码块,该作用域块内通过let/const声明的变量,会先被编译,存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量
4.执行代码,词法环境中块作用域,b和d分别赋值为3和5
5.执行代码,输出a,此时需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找。
6.当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出
总结
对于const/let变量,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。