本文详细解析 JavaScript 引擎如何管理代码执行,涵盖全局、函数及 Eval 三种执行上下文类型。深入探讨执行栈的 LIFO 机制,以及创建阶段中词法环境与变量环境的区别,解释 this 绑定、变量声明提升及 let/const 与 var 的行为差异,帮助开发者深入理解作用域与闭包原理。
作为 JavaScript 开发者,掌握程序内部的执行机制是必备技能。深入理解执行上下文和执行栈,对于进一步掌握变量声明提升、作用域以及闭包等核心概念具有关键意义。
准确掌握执行上下文和执行栈的概念,有助于提升 JavaScript 开发能力,成为更优秀的开发者。
接下来直接进入主题。
内容来源:Bit 博客
利用 Bit 应用提供的组件作为构建模块,你即可担任架构师角色。随时与团队分享、发现并开发组件,欢迎体验。
Bit - 代码组件的分享与创造:Bit 协助你在不同项目和应用中分享、发现及使用代码组件,以构建新功能……
执行上下文与执行栈基础
什么是执行上下文?
简而言之,执行上下文是评估和执行 JavaScript 代码环境的抽象概念。JavaScript 代码运行时,始终处于某个执行上下文中。
执行上下文的类型
JavaScript 中存在三种类型的执行上下文。
- 全局执行上下文:这是默认的基础上下文,所有位于函数外部的代码均在此上下文中运行。它主要执行两项操作:创建全局的 window 对象(在浏览器环境中),并将 this 的值设置为该全局对象。一个程序中仅存在一个全局执行上下文。
- 函数执行上下文:每次函数被调用时,都会为该函数创建一个新的执行上下文。每个函数拥有独立的执行上下文,但仅在函数被调用时创建。函数上下文的数量可以是任意的。每当创建新的执行上下文,都会按照特定顺序(后文详述)执行一系列步骤。
- Eval 函数执行上下文:在 eval 函数内部执行的代码拥有独立的执行上下文。由于 JavaScript 开发者极少使用 eval,此处不作深入讨论。
执行栈
执行栈,在其他编程语言中常被称为“调用栈”,是一种采用 LIFO(后进先出)数据结构设计的栈,用于存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎首次遇到脚本时,会创建一个全局执行上下文并将其压入当前的执行栈。每当引擎遇到函数调用,便为该函数创建新的执行上下文并压入栈顶。
引擎会执行位于栈顶执行上下文对应的函数。当该函数执行完毕,其执行上下文从栈中弹出,控制流程转移至栈中的下一个上下文。
通过以下代码示例来理解:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
上述代码的执行上下文栈结构。

当上述代码在浏览器中加载时,JavaScript 引擎创建全局执行上下文并将其压入当前执行栈。当遇到 first() 函数调用时,引擎为该函数创建新的执行上下文并压入栈顶。
当在 first() 函数内部调用 second() 函数时,引擎为 second() 创建新的执行上下文并压入栈顶。当 second() 函数执行完毕后,其执行上下文从栈中弹出,控制流程返回至下一个执行上下文,即 first() 函数的执行上下文。
当 first() 执行完毕,其执行上下文从栈中弹出,控制流程返回至全局执行上下文。当所有代码执行完毕后,JavaScript 引擎从当前栈中移除全局执行上下文。
执行上下文的创建机制
如何创建执行上下文?
前文已介绍 JavaScript 如何管理执行上下文,现在让我们了解 JavaScript 引擎是如何创建执行上下文的。 创建执行上下文包含两个阶段:1) 创建阶段 和 2) 执行阶段。
The Creation Phase(创建阶段)
在 JavaScript 代码执行之前,执行上下文会经历创建阶段。在此阶段会发生三件事:
- 确定 this 的值,即所谓的 This 绑定。
- 创建词法环境组件。
- 创建变量环境组件。
因此,执行上下文在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
This 绑定:
在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this 引用 Window 对象)。
在函数执行上下文中,this 的值取决于函数的调用方式。如果通过引用对象调用,则 this 被设置为该对象;否则,this 的值被设置为全局对象或在严格模式下为 undefined。例如:
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被
// 对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为
// 没有指定引用对象
词法环境与变量环境详解
词法环境
官方 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 outer function environment reference>
}
}
变量环境:
它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
如上所述,变量环境也是一个词法环境,所以它具备上述定义的词法环境的所有属性。
在 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 = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意 — 只有遇到调用函数 multiply 时,函数执行上下文才会被创建。
可能你已经注意到 let 和 const 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined。
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。
这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。
这就是我们说的变量声明提升。
执行阶段与变量提升
执行阶段
这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。
注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined。
结论
我们已经讨论过 JavaScript 程序内部是如何执行的。虽然要成为一名卓越的 JavaScript 开发者并不需要学会全部这些概念,但是如果对上面概念能有不错的理解将有助于你更轻松、更深入地理解其他概念,如变量声明提升、作用域和闭包。
就是这样,如果你发现这篇文章有用,请点击 👏 按钮并在下面自由地评论!我很乐意和你讨论 😃。
分享自 Bit 的博客
Bit 使得在项目和应用中分享小型组件和模块变得非常简单,使您和您的团队可以更快地构建代码。随时与团队分享、发现并开发组件,欢迎体验。
Bit - 代码组件的分享与创造:Bit 协助你在不同项目和应用中分享、发现及使用代码组件,以构建新功能……
总结
以上所述是小编给大家介绍的 JavaScript 中的执行上下文和执行栈,希望对大家有所帮助。如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!