js作用域和执行上下文中的let、const关键字拆解(2)

112 阅读4分钟

一、作用域之历史

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

我们分析一下编译阶段的执行上下文和代码的执行过程。

  1. 创建全局执行上下文,变量环境中存放foo函数的声明。
  2. 执行至foo函数时,创建foo函数的函数执行上下文 在这里插入图片描述 在编译阶段函数,该函数的执行上下文中
  • 通过var声明的变量,放到了变量环境中;
  • 通过let声明的变量,放到了词法环境中。 并且我们看到foo函数作用域内部的块级作用域中let声明的变量d并没有放入到词法环境中。
  1. 当执行到块级代码时,a和c被赋值,此时函数执行上下文为 在这里插入图片描述 在块级作用域中通过let声明的b和d和作用域外声明的b独立存在,互不影响。

    其实,在块级作用域内部声明了一个小型的栈结构。函数通过let和const声明的变量,最外层的压入栈底,进入一个作用域,就压入栈顶。当作用域执行完成后,就从栈顶弹出。

  2. 继续执行代码,给foo函数执行上下文中词法环境的栈顶中的b和d赋值。执行至console.log(a)时,查找变量的方式为:函数执行上下文的词法环境>函数执行上下文的变量环境>全局执行上下文。 在这里插入图片描述 输出a时,查找到foo函数执行上下文的变量环境中。 输出b时,查找到foo函数执行上下文的词法环境的栈顶时,就已经找到。

  3. 块级作用域执行完成后,从栈顶弹出,现在的调用栈为 在这里插入图片描述

  4. 输出,执行完成。

四、暂时性死区

上代码,看看输出啥:

let myname= '嘻嘻'
{
  console.log(myname) 
  let myname= '不嘻嘻'
}

如果你看到了输出,以你的聪明才智,应该能大概理解什么是暂时性死区。

console.log(myname)这行代码会报错。 暂时性死区,是指在作用域内,通过let和const声明的变量,在声明变量之前,该变量处于不可用状态。 不会提升到作用域顶部,和var声明的变量造成的变量提升相反。

文章参考:time.geekbang.org/column/intr…