五星级:通过变量提升引出执行上下文、执行上下文栈、作用域(全局作用域、函数作用域、块级作用域)

539 阅读5分钟

一、变量提升

是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

二、JavaScript 代码的执行流程

1、编译阶段

从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

2、执行阶段

JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行

三、执行上下文

1、概念

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

在执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容,比如上面代码中的变量 myname 和函数 showName,都保存在该对象中

2、创建执行上下文

2.1、当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。

2.2、当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。

2.3、当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文

3、执行上下文栈、调用栈

3.1、JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。

3.2、调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

4、开发者利用调用栈

4.1、在分析复杂结构代码,或者检查 Bug 时,调用栈都是非常有用的。

(1)“开发者工具”-》点击“Source”标签-》加上断点,并刷新页面-》右边的“call stack”下面显示出来了函数的调用关系

(2)使用 console.trace() 来输出当前的函数调用关系

5、重要性

栈是一种非常重要的数据结构,不光应用在 JavaScript 语言中,其他的编程语言,如 C/C++、Java、Python 等语言,在执行过程中也都使用了栈来管理函数之间的调用关系。所以栈是非常基础且重要的知识点,你必须得掌握

四、引入作用域(块级作用域、全局作用域、函数作用域)来讲解为什么出现变量提升

1、作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

2、全局作用域(ES6之前)、函数作用域(ES6之前)、块级作用域

2.1、全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

2.2、函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

2.3、块级作用域就是使用一对大括号({ })包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

3、出现变量提升原因

当初设计这门语言的时候,并没有想到 JavaScript 会火起来,所以只是按照最简单的方式来设计。没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。

4、变量提升带来的问题

4.1、变量容易在不被察觉的情况下被覆盖掉

var myname = "极客时间"
function showName(){
  console.log(myname);
  if(0){
   var myname = "极客邦"
  }
  console.log(myname);
}
showName()

本该像 C 代码那样打印出来“极客时间”的字符串。但是输出的内容却是 undefined

4.2.、本应销毁的变量没有被销毁

function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
}
foo()

如果你使用 C 语言或者其他的大部分语言实现类似代码,在 for 循环结束之后,i 就已经被销毁了,但是在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 7。

5、ES6 是如何解决变量提升带来的缺陷

1、ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域

function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // 不同的变量
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

6、JavaScript 是如何支持块级作用域的

块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了

当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

7、暂时性死区

【最终打印结果】:VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization

【分析原因】:在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区。

【拓展】
var的创建和初始化被提升,赋值不会被提升。

let的创建被提升,初始化和赋值不会被提升。

function的创建、初始化和赋值(变量「初始化」并「赋值」为 function(){ console.log(2) })均会被提升。