在这篇文章中,我将深入探讨JavaScript中一个最基本的部分,即Execution Context。 在本文结束时,您应该更清楚地知道解释器是怎么工作的,为什么某些函数/变量在声明之前就可以使用以及它们的值是如何确定的。
1、什么是执行上下文
当Javascript代码运行的时候,所处在当前运行时的环境,就是执行上下文
通俗一点讲就是当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,就叫做“执行上下文(execution context)”
2、执行上下文的类型
JavaScript 中有三种执行上下文类型。
- 全局执行上下文(Global):代码首次执行时候的默认环境,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- 函数执行上下文(Function):每当一个函数被调用时,都会为该函数创建一个新的上下文,每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
- eval执行上下文(Eval):当eval函数内部的文本执行的时候
你可以拥有任意数量的函数上下文。每一次函数调用都会创建一个新的上下文,它会创建一个私有域,函数内部做出的所有声明都会放在这个私有域中,并且这些声明在当前函数作用域外无法直接访问。
// global context
var sayHello = 'hello';
function person() { // execution context
var first = 'xiaoming',
last = 'xiaoli';
function firstName() { // execution context
return first;
}
function lastName() { // execution context
return last;
}
console.log(`${sayHello} ${firstName()} ${lastName()}`);
}
3、执行栈
执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。
JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1(); // 3 2 1
/*
* 通过执行栈与上下文的关系来解释上述代码的执行过程,为了方便理解,
* 我们假设执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,
* 因此过程大致如下:
*/
//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
关于执行栈,有5点需要记住:
- 单线程
- 同步执行
- 一个全局上下文
- 无数的函数上下文
- 每次函数调用都会创建一个新的执行上下文,即使是调用自身
4、执行上下文创建阶段
执行上下文创建有两个阶段:(1)创建阶段 和 (2)执行阶段。
4.1 创建阶段
js执行上下文的创建阶段主要负责三件事:
- this 值的决定,即我们所熟知的 This 绑定。
- 创建词法环境组件(LexicalEnvironment)。
- 创建变量环境组件(VariableEnvironment)。
所以执行上下文在概念上表示如下:
ExecutionContext = {
// 确定this的值
ThisBinding = <this value>,
// 创建词法环境组件
LexicalEnvironment = {},
// 创建变量环境组件
VariableEnvironment = {},
};
4.1.1 This 绑定:
在全局执行上下文中,this
的值指向全局对象。(在浏览器中,this
引用 Window 对象)。
在函数执行上下文中,this
的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this
会被设置成那个对象,否则this
的值被设置为全局对象或者undefined
(在严格模式下)。
js五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解
4.1.2 词法环境
官方es6 文档把词法定义为:
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。
词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
在词法环境的内部有两个组件:(1)环境记录器 和 (2)一个外部环境的引用。
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:全局词法环境 和 函数词法环境。
-
全局词法环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且
this
的值指向全局对象。 - 函数词法环境在函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型:
- 在全局环境中,环境记录器是对象环境记录器,用来定义出现在全局上下文中的变量和函数的关系。
- 在函数环境中,环境记录器是声明式环境记录器,用来存储变量、函数和参数。
对于函数环境-声明式环境记录器还包含了一个传递给函数的 arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。
词法环境在伪代码中看起来像这样:
// 全局环境
GlobalExectionContext = {
// 全局词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: "Object", //类型为对象环境记录
// 标识符绑定在这里
},
outer: < null >
}
};
// 函数环境
FunctionExectionContext = {
// 函数词法环境
LexicalEnvironment: {
// 环境纪录
EnvironmentRecord: {
Type: "Declarative", //类型为声明性环境记录
// 标识符绑定在这里
},
outer: < Global or outerfunction environment reference >
}
};
4.1.3 变量环境
变量环境同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系,它具备词法环境所有属性。
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。
我们通过一串伪代码来理解它们:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
// 我们用伪代码来描述上述代码中执行上下文的创建过程:
//全局执行上下文
GlobalExectionContext = {
// this绑定为全局对象
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
//环境记录
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 let const创建的变量a b在这
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
// 全局环境外部环境引入为null
outer: <null>
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 var创建的c在这
c: undefined,
}
// 全局环境外部环境引入为null
outer: <null>
}
}
// 函数执行上下文
FunctionExectionContext = {
//由于函数是默认调用 this绑定同样是全局对象
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 arguments对象在这
Arguments: {0: 20, 1: 30, length: 2},
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 var创建的g在这
g: undefined
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
}
}
可能你已经注意到,在创建阶段,let
和 const
定义的变量没有任何关联的值,但 var
定义的变量被设置为 undefined
。
这是因为,在创建阶段,代码会扫描变量和函数声明,而函数声明则完整地存储在环境中,变量最初设置为 undefined
(在情况下 var
)或保持未初始化(在情况下)let
和 const
)。
这就是为什么你可以 var
在声明之前访问已定义的变量(尽管 undefined
)但在声明之前访问let和const变量时会出现引用错误。
这就是我们所说的提升。
4.2 执行阶段
在此阶段,完成对所有这些变量的分配,最后执行代码。
在执行阶段,如果 JavaScript 引擎 let
在源代码中声明的实际位置找不到变量的值,那么它将分配给它的值 undefined
。
5、关于变量对象与活动对象
变量对象与活动对象的概念是 ES3 提出的老概念,从 ES5 开始就用词法环境和变量环境替代
在上文中,我们通过介绍词法环境与变量环境解释了为什么var会存在变量提升,为什么let const没有,而通过变量对象与活动对象是很难解释的,由其是在JavaScript在更新中不断在弥补当初设计的缺陷。
其次,词法环境的概念与变量对象这类概念也是可以对应上的。
我们知道变量对象与活动对象其实都是变量对象,变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
那这不正好对应到了全局词法记录与函数词法记录了吗。而且由于ES6新增的let const不存在变量提升,于是正好有了词法环境与变量环境的概念来解释这个问题。
所以说到这,你也不用为词法环境,变量对象的概念闹冲突了。
6、总结
-
全局执行上下文一般由浏览器创建,代码执行时就会创建;函数执行上下文只有函数被调用时才会创建,调用多少次函数就会创建多少上下文。
-
调用栈用于存放所有执行上下文,满足 FILO 规则。
-
执行上下文创建阶段分为绑定this,创建词法环境,变量环境三步,两者区别在于词法环境存放函数声明与 const let 声明的变量,而变量环境只存储var声明的变量。
-
词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不一样,全局为 null,函数为全局环境或者其它函数环境。环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。
-
你应该明白了为什么会存在变量提升,函数提升,而 let const 没有。
-
ES3 之前的变量对象与活动对象的概念在ES5之后由词法环境,变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。
所以我们已经讨论了 JavaScript 程序是如何在内部执行的。虽然你不需要学习所有这些概念才能成为一个了不起的 JavaScript 开发人员,但对上述概念有一个很好的理解将帮助你更容易、更深入地理解其他概念,如提升、作用域和闭包。
网友评论