JavaScript 执行上下文
对于那些 JavaScript 开发人员或者想要深入了解 JavaScript 工作过程的人来说,这个主题非常重要。
在这个部分,我们将学习和理解 JavaScript 的执行上下文,我们将讨论它是什么,它的类型,执行栈,执行上下文是如何创建的以及执行阶段的所有内容。我们将逐一讨论每个要点。让我们先从介绍部分开始。
到底什么是执行上下文?
执行上下文是用来描述代码的内部工作原理的概念。在 JavaScript 中,使 JavaScript 代码能够执行的环境是我们所称之为 JavaScript 执行上下文。在执行上下文中,决定了哪段代码可以访问代码中使用的函数、变量和对象。在执行上下文中,特定的代码被逐行解析,然后变量和函数被存储在内存中。执行上下文类似于一个容器,用于存储变量,代码被评估然后执行。因此,执行上下文为特定代码提供了执行环境。
执行上下文的类型
JavaScript 中的执行上下文类型有:
- 全局执行上下文(GEC)
- 函数执行上下文(FEC)
- eval 执行上下文
开始逐一讨论每个类型:
全局执行上下文
GEC / 全局执行上下文也被称为基础/默认执行上下文。任何不在任何函数中的 JavaScript 代码将存在于全局执行上下文中。它被称为“默认执行上下文”是因为当文件首次加载到 web 浏览器中时,代码开始执行。GEC 执行以下两个任务:
- 首先,创建一个全局对象,对于 Node.js 来说是全局对象,对于浏览器来说是 Window 对象。
- 其次,将 Windows 对象引用给 ‘this’ 关键字。
- 创建一个内存堆以存储变量和函数引用。
- 然后,它将所有函数声明存储在内存堆区域,将变量存储在 GEC 中,初始值为 ‘undefined’。
通过上述介绍,应该了解到全局执行上下文在每段代码中只有一个,因为 JS 引擎是单线程的,因此只有一个全局环境可以执行 JavaScript 代码。
函数执行上下文
FEC 或函数执行上下文是当 JavaScript 引擎发现任何函数调用时创建的上下文类型。每个函数都有自己的执行上下文,所以与 GEC 不同,FEC 可以有多个。另外,FEC 可以访问 GEC 的整个代码,但 GEC 却无法访问 FEC 的所有代码。在 GEC 代码执行期间,会发起一个函数调用,当 JS 引擎找到它时,为该特定函数创建一个新的 FEC。
eval 函数执行上下文
在 eval 函数中执行的任何 JavaScript 代码都会创建并持有它自己的执行上下文。但是,eval 函数不被 JavaScript 开发人员使用,但它是执行上下文的一部分。
执行栈
执行堆栈也被称为 调用堆栈 。
堆栈是以LIFO(后进先出)形式存储值的数据结构。同样,执行堆栈是在脚本生命周期中跟踪所有执行上下文的堆栈。JavaScript开发者必须知道JavaScript作为单线程运行的事实,在 Web 浏览器中一次只能执行一个任务。因此,为其他操作、函数和事件创建堆栈,称为 执行堆栈 。执行堆栈的底部是全局执行上下文(GEC),默认会存在于堆栈中。所以,在开始执行 JS 代码时(即 GEC 的执行期间),当代码中存在任何函数并且 JS 引擎搜索到该函数时,它会立即为该函数创建一个函数执行上下文(FEC),并将其推到执行上下文堆栈的顶部。始终在执行上下文堆栈的顶部可用的特定执行上下文将始终首先被 JS 引擎执行。在所有代码执行完成后,JS 引擎弹出函数的执行上下文,然后继续下一个,依此类推。通常,当脚本在浏览器中加载时,第一个元素将是全局执行上下文。但当检测到函数执行时,执行上下文将被创建,并虚拟地放置在 GEC 的顶部。这个过程将继续,直到整个代码的执行完成。
为了理解执行堆栈的工作过程,让我们考虑下面给出的示例代码:
let x = 'Hello World!';
function a() {
console.log('It is the first function');
function b() {
console.log('It is the second function');
}
b();
}
a();
console.log('It is GEC);
这是一个示例代码,用于理解其工作原理。
解释:
- 首先,所有的代码都被加载到浏览器中。
- 然后,JS引擎将GEC推送/插入到执行栈的顶部。
-
当JS引擎遇到第一个函数调用时,它为其设置一个新的FEC,并将其添加到当前执行栈的顶部。
-
接下来,我们可以看到在第一个函数内部调用了第二个函数。因此,JS引擎为第二个函数设置了一个新的FEC,并将其插入到栈的顶部。
-
当第二个函数执行完成时,执行函数从栈中弹出,控制权转移到栈中的下一个执行上下文,即仅为第一个函数的执行上下文。
-
当第一个函数完全执行时,第一个函数的执行栈从栈中弹出。因此,控制权回到代码的GEC。
-
最后,当整个代码的执行完成时,JS引擎从当前栈中移除GEC。
这样,执行栈的执行就完成了。
创建执行上下文
首先创建执行上下文,然后进行管理。创建执行上下文有两种方法:
创建阶段
创建阶段是JS引擎调用函数但不开始执行的阶段。在这个阶段,JS引擎开始编译阶段,并扫描特定函数代码以进行编译,但不执行代码。执行上下文的创建是JavaScript引擎的责任,它通过执行以下描述的任务来创建:
任务1:创建激活对象/变量对象: JavaScript的特殊对象之一,类似于一个容器,用于保存函数参数、变量以及内部函数声明的所有信息。但没有dunder proto属性。
任务2:创建作用域链: 在完成任务1后,JS引擎初始化作用域链。作用域链是一个列表,其中包含了当前函数所在的所有变量对象。作用域链还包括GEC的变量对象,并携带当前函数的变量对象。
- 确定’this’的值: 作用域链创建后,JS引擎初始化’this’的值。
让我们通过以下示例代码来理解激活对象的创建:
示例代码1
function test (x, y) {
var z = 10;
var w = 5;
w = function() {
return x - y;
}
}
test(12, 3);
现在,JS引擎在调用test()之后,在执行其代码之前创建了一个 executionContextObj ,可以在下面的代码中看到:
executionContextObj = {
variableObject: {}, // contains all variables, inner functions, arguments details of test()
scopechain: [], // contains list of all the scopes for test()
this // Value of this
}
激活对象包含参数对象,该对象进一步包含有关函数参数的详细信息。它对于当前函数内部声明的每个函数和变量都携带属性名称。在我们的情况下,激活对象,例如,代码1将是:
variableObject = {
argumentObject : {
0: x,
1: y,
length: 2
},
x: 3,
y: 2
z: undefined,
w: undefined so the pointer points to the function definition of w
}
说明:
- 如上述代码所示,JS引擎已经创建了参数对象。此外,还存在一个包含函数中参数总数的length属性。它只有属性名,没有属性值。
- 在函数中,对于每个初始值为’undefined’的变量,JS引擎在激活对象或变量对象上设置属性。这些参数也是函数中的变量,因此也是参数对象的属性。
- 如果变量已经存在作为参数对象的属性,那么JS引擎会继续执行,不会采取进一步的操作。
- 当JS引擎在当前函数中找到一个函数定义时,通过函数的名称创建一个新的属性。如上所述,函数定义存储在堆内存中。函数名称属性指向堆内存中的函数定义。
- 因此,如上述代码所示,w是一个变量。因此,它将得到’undefined’的值。但是,当找到具有相同名称的函数时,将进行覆盖,并且其值将指向在堆内存中存储的函数w的定义。然后,JS引擎设置作用域链并确定’this’的值。
所以,这就是创建阶段的工作方式。
执行阶段
执行阶段是在完成创建阶段后的下一个阶段。执行阶段是JS引擎再次扫描代码中的函数的阶段,即用变量的值更新变量对象,并运行代码。让我们看看执行阶段或我们上面讨论的示例的完整代码:
示例代码2
x = 10;
var y = 20;
z = function(val) {
var p = 5;
var q= 10;
x = 30
function test () {
var r = 2;
}
test();
}
z(3);
首先,上述代码在浏览器中加载。然后,JavaScript引擎开始编译阶段以创建执行对象。在编译阶段,JavaScript引擎只处理和管理声明,不关心值。
现在,在执行阶段,将按照每行描述的方式执行以下步骤:
- 变量x被赋予10,这使得JavaScript引擎不会将其视为函数声明或变量声明而继续执行,即到第三行。在第三行,它不执行任何操作,因为那不是任何声明。
- 接下来,JavaScript引擎在GEC对象中设置一个属性’z’(变量名),因为z是一个变量名,还在全局声明范围内进行,并将其初始化为’undefined’。
- 转到代码的第五行,JavaScript引擎遇到一个函数声明。JavaScript引擎会将函数定义存储在堆内存中,然后设置一个指向该特定堆内存位置的属性。无论函数中存储了什么,它只是指向其位置。
- 正如我们在最后一行中看到的,那不是任何代码的声明。因此,JavaScript引擎不执行任何操作。
因此,这样以创建阶段和执行阶段的方式进行工作。
创建阶段后的GEC对象
在上述解释中,我们看到了如何通过执行和创建阶段创建执行堆栈,例如代码示例2。然而,我们应该深入了解GEC和FEC的工作原理,只针对上述代码。考虑以下示例代码2的GEC对象代码:
globalExecutionContextObj = {
activationbj: {
argumentObj : {
length:0
},
y: undefined,
z: Pointer to the function definition
},
scopeChain: [Global execution context variable object],
this: value of this
}
如上所示,没有任何代码被留下,JS引擎会再次扫描函数进入执行阶段。JS引擎会按照以下描述依次对每一行代码进行更新变量值并执行:
- 首先,JS引擎发现变量对象中没有名为x的属性,所以它在GEC中添加了这个属性并将其值初始化为10。
- 接下来,JS引擎发现变量对象中存在名为y的属性,因此更新其值为20。
- 最后,JS引擎不采取任何行动,因为它是一个函数声明。
现在,让我们在执行阶段之后看一下GEC对象。
执行阶段后的GEC对象
在我们的示例代码2中:
globalExecutionContextObj = {
activationbj: {
argumentObj : {
length:0
},
y: 20,
z: Pointer to the function definition,
x: 10
},
scopeChain: [Global execution context variable object],
this: value of this
}
- 现在,当 z()再次被调用时,JS引擎再次进入编译阶段。因此,它会扫描函数以创建其执行上下文对象。
- 函数 z()有 ‘val’作为其参数,因此JS引擎将 ‘val’添加到 z()执行上下文对象的参数对象中。然后,它通过名称 ‘val’创建一个属性。
- 接下来,它检查并找到函数的激活对象中是否有属性 p。因此,发现不存在这样的属性,所以它将 p 添加为属性,然后将其值初始化为 ‘undefined’。
- 接下来,JS引擎的职责是查看 q 是否是函数的激活对象中的属性。因此,发现不存在这样的属性,所以它将 q 添加为属性,然后将其值初始化为 ‘undefined’。
- 然后,JS引擎继续执行下一行,因为 x = 30 不是一个声明。
- 接下来,JS引擎遇到了一个 test() 函数声明,并且对于它,它将函数定义存储在堆内存区域中。然后,它设置一个名为 ‘test’ 的属性,指向函数定义存储的位置。JS引擎不关注其中存储的值是什么。
编译阶段后的 z 执行对象
以下是完成编译阶段后的 FEC 对象的代码:
zExecutionContextObj = {
activationbj: {
argumentObj : {
0: val,
length:1
},
val: 3,
p: undefined,
q: undefined
test: Pointer to the function definition,
},
scopeChain: [z variable object, Global execution context variable object],
this: value of this
}
示例代码2在编译阶段后的代码执行过程如下所述:
- 这是一个test ()函数的调用,而不是声明,因此JS引擎不会执行任何操作。
- 现在,JS引擎将通过扫描执行z ()函数来进行执行阶段。
- 在执行阶段,变量p和q将得到5和10作为它们的值。
- 接下来,JS引擎发现x不是声明,也不是z执行上下文对象上的任何属性,所以通过作用域链移动到代码的GEC,并查看GEC中是否存在名为x的属性。如果没有找到,JS引擎将创建一个名为x的新属性并进行初始化。在示例代码2中,JS引擎发现一个名为x的属性已经存在于GEC对象上,所以将其值从10更新为30。需要注意的是,JS引擎在这种情况下只会继续前往GEC,即在执行阶段找到的变量不是当前执行上下文对象上的属性时才会继续前往GEC
- 然后,JS引擎设置了一个test属性,然后将其指向堆内存的位置。
执行阶段后的z执行上下文对象
在示例代码2中:
zExecutionContextObj = {
activationbj: {
argumentObj : {
0: val,
length:1
},
val: 3,
p: 5,
q: 10
test: Pointer to the function definition,
},
scopeChain: [z variable object, Global execution context variable object],
this: value of this
}
- 在示例代码2中,JS引擎再次进入编译阶段,为’test’创建执行上下文对象。
- 测试执行上下文对象可以访问z中定义的每个函数,每个变量,并且通过作用域链也在全局作用域中。
- 同样,z可以访问全局作用域中的每个变量和对象。但是,它无法访问测试的变量和对象。
- 代码的全局执行上下文没有访问z或测试的变量或对象。
因此,这就是执行上下文的创建和定义方式。
全局执行上下文与函数执行上下文的区别
以下是两者之间的区别:
全局执行上下文 | 函数执行上下文 |
---|---|
它创建了一个全局作用域。 | 它创建了一个参数对象。 |
它创建了一个被称为 ‘this’ 的对象。 | 默认情况下,它指向Window对象。 |
它为全局定义的函数和变量设置了内存空间。 | 它为只在函数内定义的函数和变量设置了内存空间。 |
GEC 在设置任何函数声明在内存中时,将变量声明的默认值赋值为 ‘undefined’。 | FEC 在设置任何函数声明在内存中时,将变量声明的默认值赋值为 ‘undefined’。同时,它还创建了自己的执行堆栈。 |