本文深入解析 JavaScript 引擎在执行代码前创建的执行上下文机制。详细阐述全局与函数执行上下文的创建过程,包括 this 绑定、词法环境与变量环境的区别。通过执行栈(LIFO)模型解释函数调用顺序,并对比 ES3 的变量对象概念,厘清变量提升、函数提升及 let/const 暂时性死区的底层原理。
引言
引言
通常认为 JavaScript 代码按书写顺序执行,但实际运行中,即使是同步代码,执行顺序也常与预期不符。例如:
function f1() {
console.log('听风是风');
};
f1(); //echo
function f1() {
console.log('echo');
};
f1(); //echo
按书写顺序,先输出“听风是风”,再输出“echo”。但实际两次均输出“echo”。若将函数声明改为函数表达式,结果则不同:
var f1 = function () {
console.log('听风是风');
};
f1(); //听风是风
var f1 = function() {
console.log('echo');
};
f1(); //echo
这表明代码执行前 JS 引擎进行了预处理。要理解这一机制,必须了解 JS 执行上下文。
JS 执行上下文概述
JS 执行上下文概述
JS 引擎在执行代码前,会创建对应的执行上下文。
执行上下文仅分三类:全局执行上下文、函数执行上下文和 eval 上下文。因 eval 极少使用,本文不作讨论。
1. 全局执行上下文
全局执行上下文唯一,由浏览器创建,对应 window 对象,可通过 this 直接访问。

window 对象预定义了大量属性和方法,全局环境中可直接访问。此外,var 声明的全局变量也挂载在 window 对象上,可通过 window 直接访问。

2. 函数执行上下文
函数执行上下文数量不限,每次函数调用都会创建一个新的上下文。即使同一函数多次调用,也会生成多个独立的上下文实例。
不同种类的上下文如何关联并由谁管理?这涉及执行上下文栈的概念。
执行上下文栈(调用栈)
执行上下文栈(调用栈)
执行上下文栈(简称执行栈)用于存储代码执行期间创建的所有上下文,遵循后进先出(LIFO)原则。
JS 首次运行时,创建全局执行上下文并压入栈底。随后每次函数调用,都会创建新的函数执行上下文并压入栈顶。由于 LIFO 特性,全局执行上下文始终位于栈底,直到脚本结束。
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();
// 此时执行栈中只剩下一个全局执行上下文
至此,我们了解了执行栈与上下文的存储规则。前文提到 JS 引擎在代码执行前会创建上下文,具体如何创建?
执行上下文创建阶段
执行上下文创建阶段
执行上下文的创建分为创建阶段和执行阶段。创建阶段较难理解,先讨论此阶段。
创建阶段主要完成三件事:确定 this 绑定、创建词法环境组件(LexicalEnvironment)、创建变量环境组件(VariableEnvironment)。
此处引用伪代码表示创建过程:
ExecutionContext = {
// 确定this的值
ThisBinding = <this value>,
// 创建词法环境组件
LexicalEnvironment = {},
// 创建变量环境组件
VariableEnvironment = {},
};
阅读其他资料可能会疑惑,为何此处未提及 this、作用域或变量对象/活动对象?后文将解释这一差异。
1. 确定 this
官方称为 This Binding。全局执行上下文中,this 始终指向全局对象(浏览器中为 window)。
函数执行上下文中,this 值取决于调用方式。若被对象调用,this 指向该对象;否则指向全局对象 window 或 undefined(严格模式下)。
2. 词法环境组件
词法环境是存储标识符(变量/函数名)与值映射的结构。标识符指向实际对象(含函数)或原始值。
词法环境由两部分组成:环境记录(存储当前环境的变量和函数声明)和外部环境引用(指向外部环境的记录)。
其中环境记录用于存储当前环境中的变量和函数声明的实际位置;外部环境引入记录很好理解,它用于保存自身环境可以访问的其它外部环境,那么说到这个,是不是有点作用域链的意思?
根据上下文类型,词法环境分为全局词法环境和函数词法环境。
全局词法环境组件:
外部环境引用为 null,因其是最外层环境。它记录了当前环境下的所有属性和方法。
函数词法环境组件:
包含用户定义的属性和方法,以及 arguments 对象。其外部环境引用可以是全局环境或其他函数环境,具体取决于代码结构。
引用译文中的伪代码(全局环境记录为对象环境记录,函数环境记录为声明性环境记录):
// 全局环境
GlobalExectionContext = {
// 全局词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: "Object", //类型为对象环境记录
// 标识符绑定在这里
},
outer: < null >
}
};
// 函数环境
FunctionExectionContext = {
// 函数词法环境
LexicalEnvironment: {
// 环境纪录
EnvironmentRecord: {
Type: "Declarative", //类型为声明性环境记录
// 标识符绑定在这里
},
outer: < Global or outerfunction environment reference >
}
};
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>
}
}
在创建阶段,函数声明和 var 变量已被赋值:var 为 undefined,函数为自身。let/const 处于未初始化状态。
这解释了变量提升和函数声明提前的现象,以及 let/const 存在暂时性死区的原因:JS 引擎在创建阶段对它们的初始化赋值不同。
执行阶段根据创建阶段的环境记录进行赋值。例如,var 在创建阶段为 undefined,执行时若有值则赋值;let/const 若未初始化且无值,则保持 undefined。
关于变量对象与活动对象
关于变量对象与活动对象
为何其他文章讨论上下文时涉及作用域、变量对象和活动对象,而本文使用词法环境和变量环境?
经查阅资料,变量对象与活动对象是 ES3 提出的旧概念。从 ES5 开始,使用词法环境和变量环境替代,因其解释力更强。
前文通过词法环境和变量环境解释了 var 的变量提升及 let/const 无提升的现象,而旧概念难以解释这些现代 JavaScript 特性。
此外,词法环境与变量对象概念可相互对应。
变量对象(Variable Object, VO)是执行上下文相关的数据作用域,存储变量和函数声明。在函数上下文中,活动对象(Activation Object, AO)作为 VO 的表示。
这正好对应全局词法记录与函数词法记录。由于 ES6 新增的 let/const 无变量提升,因此引入词法环境与变量环境来解释这一区别。
因此,词法环境、变量环境与旧有的变量对象、活动对象概念并不冲突。
总结上述概念。
总结
总结
1. 全局执行上下文由浏览器创建,代码执行时生成;函数执行上下文仅在函数调用时创建,调用次数等于上下文数量。
2. 调用栈存放所有执行上下文,遵循 FILO(先进后出)规则。
3. 执行上下文创建阶段包括绑定 this、创建词法环境和变量环境。区别在于:词法环境存储函数声明与 let/const 变量,变量环境仅存储 var 变量。
4. 词法环境由环境记录和外部引用组成。全局上下文外部引用为 null,函数上下文为全局或其他函数环境。环境记录类型也不同:全局为对象环境记录,函数为声明性环境记录。
5. 明确了变量提升、函数提升存在,而 let/const 不存在的原因。
6. ES3 的变量对象与活动对象概念在 ES5 后被词法环境和变量环境取代,后者更易于理解,两者不冲突。
本文内容到此结束,希望对学习有所帮助,欢迎支持本站。