一、作用域之历史
ES6之前,js只有两种作用域:全局作用域和函数作用域。
- 全局作用域:在js定义的全局函数或变量,在代码的任何地方可以访问。生命周期伴随页面的声明周期。
- 函数作用域:函数内部定义的函数或变量,只能在函数内部访问。函数执行结束,内部定义的变量即被销毁(闭包除外)。
ES6引入了块级作用域的概念:
- 块级作用域:在代码块{}中定义的函数或变量,只能在块中访问。
所以说作用域就是定义变量和函数的区域,该区域决定了变量的生命周期。
二、变量提升带来的问题
因为ES6之前还没有块级作用域,所以var带来的变量提升引发了几个问题。
1. 变量覆盖
上代码看看输出啥:
var myname = "王中王"
function showName(){
console.log(myname);
if(0){
var myname = "旺中旺"
}
console.log(myname);
}
showName()
你先试试。
我们分析下过程:
当开始执行showName()函数时,此时程序的调用栈如下图所示:
js会优先从当前的执行上下文中查找变量。由于myname在showname函数中的变量提升,代码编译后即在showname函数执行上下文的变量环境中。因此可以获取到myname变量,值为undefined。
2. 变量未销毁
上代码看看输出啥:
function loop(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
loop()
大部分编程语言在console.log(i)时,已经访问不到i了。 由于在js中var声明的变量存在变量提升的概念,所以i最后输出7。变量i并没有被销毁。
因此es6引入了块级作用域的概念。
三、块级作用域
为解决变量提升带来的问题,es6引入了块级作用域的概念和let、const关键字。
在第二部分王中王和旺中旺的代码中,我们看到变量提升带来的变量覆盖问题。我们看下块级作用域是怎么解决这个问题的。
let myname = "王中王";
function showName() {
console.log(myname);
if (true) {
let myname = "旺中旺";
}
console.log(myname);
}
showName();
这里代码执行后,输出的myname值都为王中王。旺中旺因为由let在块中声明,所以在编译期间并不会变量提升至函数可见。因此块内声明的变量不会影响块外的声明的变量。
执行到图中第14行时,Block作用域内(我用的vue3,此时的Block作用域等同于js的全局执行上下文)已经没有值为“旺中旺”的myname变量了。if(true)块级作用域执行完成后即销毁。
通过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()
我们分析一下编译阶段的执行上下文和代码的执行过程。
- 创建全局执行上下文,变量环境中存放foo函数的声明。
- 执行至foo函数时,创建foo函数的函数执行上下文
在编译阶段函数,该函数的执行上下文中
- 通过var声明的变量,放到了变量环境中;
- 通过let声明的变量,放到了词法环境中。 并且我们看到foo函数作用域内部的块级作用域中let声明的变量d并没有放入到词法环境中。
-
当执行到块级代码时,a和c被赋值,此时函数执行上下文为
在块级作用域中通过let声明的b和d和作用域外声明的b独立存在,互不影响。
其实,在块级作用域内部声明了一个小型的栈结构。函数通过let和const声明的变量,最外层的压入栈底,进入一个作用域,就压入栈顶。当作用域执行完成后,就从栈顶弹出。
-
继续执行代码,给foo函数执行上下文中词法环境的栈顶中的b和d赋值。执行至console.log(a)时,查找变量的方式为:函数执行上下文的词法环境>函数执行上下文的变量环境>全局执行上下文。
输出a时,查找到foo函数执行上下文的变量环境中。 输出b时,查找到foo函数执行上下文的词法环境的栈顶时,就已经找到。
-
块级作用域执行完成后,从栈顶弹出,现在的调用栈为
-
输出,执行完成。
四、暂时性死区
上代码,看看输出啥:
let myname= '嘻嘻'
{
console.log(myname)
let myname= '不嘻嘻'
}
如果你看到了输出,以你的聪明才智,应该能大概理解什么是暂时性死区。
console.log(myname)这行代码会报错。 暂时性死区,是指在作用域内,通过let和const声明的变量,在声明变量之前,该变量处于不可用状态。 不会提升到作用域顶部,和var声明的变量造成的变量提升相反。