likes
comments
collection
share

【前端必会知识】JS 执行上下文

作者站长头像
站长
· 阅读数 10

先理解一段概念

什么是 Execution Context?

打个比方:你要去参与一个舞蹈演出,是不是首先你得有一个演出的场地去供你去演出?

所以说JavaScript(你)在执行语句前(在舞蹈演出前),需要经过的一系列的“准备”,为代码执行创造的执行环境就是执行上下文(需要演出场地环境)。

什么是文本环境?

每个演出是不是都有参演人员名单?指导老师可以通过参演人员名单去安排每个参演人员需要干什么。

那么类似的,文本环境(Lexical Environment) 就相当于演出中的参演人员名单,用于在 JS 代码执行之前把变量名、类名、函数名、……等等登记在文本环境上。JS 在执行过程中就可以在文本环境中查找变量、函数还有类等所需的东西。

【前端必会知识】JS 执行上下文

什么是执行栈?

我们要找变量的前提是找到文本环境,而文本环境在执行上下文中,那么 JS 在哪里找到执行上下文呢?

JS 在 执行栈(Execution Context Stack) 中找执行上下文:

  • 当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

  • 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程会到达当前栈中的下一个上下文。

  • 执行栈栈顶的执行上下文称为当前执行上下文。

  • JS 代码总是在当前上下文中运行(也就是说 JS 代码中所需要用到的资源是到当前执行上下文中查找)。

【前端必会知识】JS 执行上下文

什么时候会创建新的执行上下文?

3种情况下会创建新的执行上下文

  • 进入全局代码
  • 进入 Function 函数体代码
  • 进入 Eval 函数参数指定的代码

小结

  1. 执行上下文 是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

  2. 文本环境 是 JavaScript 引擎用于存储变量和函数的容器。

  3. 执行栈 是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

  4. JavaScript 中有三种执行上下文类型。

    • 全局执行上下文 :任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值指向这个全局对象。一个程序中只会有一个全局执行上下文。

    • 函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

    • Eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以不讨论它。

怎么创建执行上下文?

我们先来一个例子,解释说明一下大概流程:

【前端必会知识】JS 执行上下文


Step1:创建执行上下文,加入栈顶

首先会创建全局执行上下文,加入栈顶。它的文本环境由两部分组成:第一部分是全局对象(在浏览器环境下,就是表示 Windows 对象),第二部分是全局 Scope。

【前端必会知识】JS 执行上下文

有两个特殊的地方:

  • var 和 function 声明创建在全局对象中,而 let 、const 、class 声明的变量创建在全局 Scope 中。

  • 在执行过程中,需要寻找变量,首先先到全局 Scope 中找,找不到再到全局对象中找。

举些例子理解:

// let 声明的变量创建在全局 Scope 中
// 所以 window 上面没有 aLet 会输出 undefined
let aLet = 'aLet';
console.log(aLet); // aLet
console.log(window.aLet); // undefined

【前端必会知识】JS 执行上下文

// var 声明创建在全局对象
// 需要寻找变量时,首先先到全局 Scope 中找,找不到再到全局对象中找。
// 所以这里都有输出
var aVar = "aVar";
console.log(aVar); // aVar
console.log(window.aVar); // aVar

Step2:分析

  • 找到所有的非函数中的var声明。

  • 找到所有的顶级函数声明(顶级函数声明就是不包括在大括号内的函数声明)

  • 找到顶级let const classj声明

  • 找到块中声明的,函数名不与上述重复(中)

Step3:名字重复处理

注意:

  • 1et const class 声明的名字之间不能重复

  • let const class 和 var function 的名字不能重复

  • var 和 Function 名字重复的,function 声明的函数名优先

Step4:创建绑定,登记到全局上下文里面

  • var 登记并初始化为 undefined。

  • 顶级函数声明:登记function名字,并初始化为新创建函数对象。

  • 块级中函数声明:登记函数名字,初始化为undefined。

  • let const class 登记但未初始化。

这里就可以解释为什么 var声明的变量 会有变量提升的情况,而 let const class 声明的变量没有变量提升的情况。这是因为 let const class 声明的变量在登记的时候就没有初始化,是不可以使用的!

Step5: 执行代码

最后就是可以开始执行代码了。根据代码的执行顺序,逐行执行,并根据文本环境进行变量的查找和赋值等等操作。


那么最开始的那个例子会在全局上下文中登记 var 声明的变量 a 并初始化为 undefined,然后开始执行语句,所以输出为 undefined 而不是 'foo'。

console.log(a); // undefined
if (false) {
  var a = "foo";
}

然后需要注意的是:

  • 每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

  • 函数对象在被创建的时候,函数对象的体内会保存函数创建时的执行上下文的文本环境。

  • 函数的执行上下文的文本环境没有全局对象只有函数 Scope。函数里面的变量,不管 let const class var 还是 函数声明 都是创建在这个函数 Scope 里面。

举个例子:

【前端必会知识】JS 执行上下文

这个例子在创建 foo 函数的执行上下文的时候,里面的文本环境就只有一个 a 变量,但是因为是 let 声明的,所以未被初始化,在执行 foo 函数时就会报错。

var a = 10;
function foo() {
  console.log(a);
  let a;
}
foo(); // 会报错 ReferenceError: Cannot access 'a' before initialization

小结

创建执行上下文的过程可以分为以下几个步骤:

  1. 创建执行上下文:在代码执行之前,会先创建全局执行上下文,将其加入执行上下文栈的栈顶。全局执行上下文的文本环境由两部分组成:全局对象(在浏览器环境下,表示为 window 对象)和全局作用域。然后也会创建函数执行上下文:函数对象在被创建时,会保存函数创建时的执行上下文的文本环境。函数的执行上下文的文本环境没有全局对象,只有函数作用域。

  2. 分析变量声明:在创建执行上下文时,会先找到所有非函数中的 var 声明,然后找到顶级函数声明(不包括在大括号内的函数声明)。接着找到顶级的 letconstclass 声明。最后,在块级作用域中找到函数声明。

  3. 处理名字重复:在执行上下文中,需要处理变量名字的重复情况。letconstclass 声明的名字之间不能重复,letconstclass 声明的名字与 var 和函数声明的名字也不能重复。如果出现了重复的情况,会按照一定的优先级规则进行处理。

  4. 创建绑定并登记到执行上下文中:在处理完变量声明之后,会创建绑定并将其登记到执行上下文中。对于 var 声明的变量,会进行登记并初始化为 undefined。对于顶级函数声明,会登记函数名字并初始化为新创建的函数对象。对于块级中的函数声明,会登记函数名字但不进行初始化。而对于 letconstclass 声明的变量,会进行登记但不进行初始化。需要注意的是,函数内部的变量,无论是使用 letconstclassvar 还是函数声明,都是在函数作用域中创建并初始化的。

  5. 执行代码:在执行上下文创建完毕后,就可以开始执行代码了。根据代码的执行顺序,逐行执行,并根据作用域链和词法环境进行变量的查找和赋值操作。

后记