先理解一段概念
什么是 Execution Context?
打个比方:你要去参与一个舞蹈演出,是不是首先你得有一个演出的场地去供你去演出?
所以说JavaScript(你)在执行语句前(在舞蹈演出前),需要经过的一系列的“准备”,为代码执行创造的执行环境就是执行上下文(需要演出场地环境)。
什么是文本环境?
每个演出是不是都有参演人员名单?指导老师可以通过参演人员名单去安排每个参演人员需要干什么。
那么类似的,文本环境(Lexical Environment) 就相当于演出中的参演人员名单,用于在 JS 代码执行之前把变量名、类名、函数名、……等等登记在文本环境上。JS 在执行过程中就可以在文本环境中查找变量、函数还有类等所需的东西。
什么是执行栈?
我们要找变量的前提是找到文本环境,而文本环境在执行上下文中,那么 JS 在哪里找到执行上下文呢?
JS 在 执行栈(Execution Context Stack) 中找执行上下文:
-
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
-
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程会到达当前栈中的下一个上下文。
-
执行栈栈顶的执行上下文称为当前执行上下文。
-
JS 代码总是在当前上下文中运行(也就是说 JS 代码中所需要用到的资源是到当前执行上下文中查找)。
什么时候会创建新的执行上下文?
3种情况下会创建新的执行上下文
- 进入全局代码
- 进入 Function 函数体代码
- 进入 Eval 函数参数指定的代码
小结
-
执行上下文 是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
-
文本环境 是 JavaScript 引擎用于存储变量和函数的容器。
-
执行栈 是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
-
JavaScript 中有三种执行上下文类型。
-
全局执行上下文 :任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this
的值指向这个全局对象。一个程序中只会有一个全局执行上下文。 -
函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
-
Eval 函数执行上下文: 执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval
,所以不讨论它。
-
怎么创建执行上下文?
我们先来一个例子,解释说明一下大概流程:
Step1:创建执行上下文,加入栈顶
首先会创建全局执行上下文,加入栈顶。它的文本环境由两部分组成:第一部分是全局对象(在浏览器环境下,就是表示 Windows 对象),第二部分是全局 Scope。
有两个特殊的地方:
-
var 和 function 声明创建在全局对象中,而 let 、const 、class 声明的变量创建在全局 Scope 中。
-
在执行过程中,需要寻找变量,首先先到全局 Scope 中找,找不到再到全局对象中找。
举些例子理解:
// let 声明的变量创建在全局 Scope 中
// 所以 window 上面没有 aLet 会输出 undefined
let aLet = 'aLet';
console.log(aLet); // aLet
console.log(window.aLet); // undefined
// 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 里面。
举个例子:
这个例子在创建 foo 函数的执行上下文的时候,里面的文本环境就只有一个 a 变量,但是因为是 let 声明的,所以未被初始化,在执行 foo 函数时就会报错。
var a = 10;
function foo() {
console.log(a);
let a;
}
foo(); // 会报错 ReferenceError: Cannot access 'a' before initialization
小结
创建执行上下文的过程可以分为以下几个步骤:
-
创建执行上下文:在代码执行之前,会先创建全局执行上下文,将其加入执行上下文栈的栈顶。全局执行上下文的文本环境由两部分组成:全局对象(在浏览器环境下,表示为
window
对象)和全局作用域。然后也会创建函数执行上下文:函数对象在被创建时,会保存函数创建时的执行上下文的文本环境。函数的执行上下文的文本环境没有全局对象,只有函数作用域。 -
分析变量声明:在创建执行上下文时,会先找到所有非函数中的
var
声明,然后找到顶级函数声明(不包括在大括号内的函数声明)。接着找到顶级的let
、const
、class
声明。最后,在块级作用域中找到函数声明。 -
处理名字重复:在执行上下文中,需要处理变量名字的重复情况。
let
、const
、class
声明的名字之间不能重复,let
、const
、class
声明的名字与var
和函数声明的名字也不能重复。如果出现了重复的情况,会按照一定的优先级规则进行处理。 -
创建绑定并登记到执行上下文中:在处理完变量声明之后,会创建绑定并将其登记到执行上下文中。对于
var
声明的变量,会进行登记并初始化为undefined
。对于顶级函数声明,会登记函数名字并初始化为新创建的函数对象。对于块级中的函数声明,会登记函数名字但不进行初始化。而对于let
、const
、class
声明的变量,会进行登记但不进行初始化。需要注意的是,函数内部的变量,无论是使用let
、const
、class
、var
还是函数声明,都是在函数作用域中创建并初始化的。 -
执行代码:在执行上下文创建完毕后,就可以开始执行代码了。根据代码的执行顺序,逐行执行,并根据作用域链和词法环境进行变量的查找和赋值操作。
后记
😖我也是看了很多文章才慢慢对 JS 执行上下文有些理解,然后我推荐一篇关于JS执行上下文的文章([译] 理解 JavaScript 中的执行上下文和执行栈)这篇讲的很专业,但是就是有点晦涩。我看的时候也是半懵半懂。最后的最后,希望本文能增加大家对 Js 执行上下文的理解。本人水平有限,如果发现问题或者需要补充的点欢迎大家通过评论告诉我!!!