执行上下文(Execution Context)
JavaScript 引擎执行代码时的底层机制:执行上下文的创建、变量环境、词法环境
1. 执行上下文概述
1.1 什么是执行上下文
定义:执行上下文是 JavaScript 引擎执行代码时的环境,包含了执行代码所需的所有信息。
类型:
- 全局执行上下文(Global Execution Context):代码首次执行时创建
- 函数执行上下文(Function Execution Context):函数调用时创建
- Eval 执行上下文(Eval Execution Context):eval 函数执行时创建(不推荐使用)
1.2 JavaScript 代码的执行流程
重要概念:一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。
执行流程:
JavaScript 代码
↓
[编译阶段]
↓
执行上下文 + 可执行代码
↓
[执行阶段]
↓
执行代码,输出结果
详细流程:
-
编译阶段:
- 词法分析(Lexical Analysis)
- 语法分析(Parsing)
- 生成 AST(Abstract Syntax Tree)
- 创建执行上下文
- 处理声明(变量提升)
-
执行阶段:
- 执行可执行代码
- 变量赋值
- 函数调用
- 表达式求值
1.3 执行上下文的结构
根据 ECMAScript 规范,执行上下文包含以下组件:
ExecutionContext {
LexicalEnvironment: {...}, // 词法环境
VariableEnvironment: {...}, // 变量环境
PrivateEnvironment: {...}, // 私有环境(ES2022,class 私有字段)
ThisBinding: value, // this 绑定
Function: function, // 函数对象(仅函数执行上下文)
Realm: realm, // Realm(隔离环境)
ScriptOrModule: module // 脚本或模块(ES6+)
}
2. 变量环境(Variable Environment)
2.1 变量环境的定义
定义:变量环境(Variable Environment)是执行上下文中存储 var 和 function 声明的地方。
特点:
- 存储
var声明的变量 - 存储
function声明(ES5 及之前) - 在创建阶段初始化为
undefined - 是执行上下文的一个组件
2.2 var 声明的处理过程
2.2.1 编译阶段(Compilation Phase)
JavaScript 引擎的处理流程:
var x = 10;
var y = 20;引擎处理步骤:
-
词法分析(Lexical Analysis)
- 识别
var、x、=、10等 tokens
- 识别
-
语法分析(Parsing)
- 构建抽象语法树(AST)
- 识别变量声明语句
-
作用域分析(Scope Analysis)
- 确定变量属于哪个作用域
- 对于
var,属于函数作用域或全局作用域
-
创建变量环境(Variable Environment Creation)
- 在变量环境中创建绑定
- 初始化为
undefined
伪代码表示:
// 编译阶段
VariableEnvironment = {
x: undefined, // var 声明,初始化为 undefined
y: undefined // var 声明,初始化为 undefined
};2.2.2 执行阶段(Execution Phase)
变量赋值过程:
// 执行阶段
x = 10; // 在变量环境中查找 x,赋值
y = 20; // 在变量环境中查找 y,赋值执行后的变量环境:
VariableEnvironment = {
x: 10,
y: 20
};2.3 全局 var 声明与全局对象
2.3.1 全局对象(Global Object)
浏览器环境:window 对象
Node.js 环境:global 对象
ES2020+:globalThis(统一访问)
2.3.2 var 如何添加到全局对象
关键机制:全局执行上下文中的变量环境就是全局对象本身。
ECMAScript 规范:
- 全局执行上下文的变量环境 = 全局对象(Global Object)
var在全局作用域声明时,会直接在全局对象上创建属性
示例:
var globalVar = 10;
console.log(window.globalVar); // 10(浏览器)
console.log(globalVar); // 10
console.log(this.globalVar); // 10(全局 this 指向 window)引擎内部实现(简化):
// 全局执行上下文创建
GlobalExecutionContext = {
VariableEnvironment: globalThis, // 变量环境 = 全局对象
LexicalEnvironment: {
// 词法环境(存储 let/const)
outer: null
},
ThisBinding: globalThis
};
// var 声明时
globalThis.globalVar = undefined; // 创建阶段
globalThis.globalVar = 10; // 执行阶段2.3.3 let/const 为什么不会添加到全局对象
关键区别:
var存储在变量环境(Variable Environment)= 全局对象let/const存储在词法环境(Lexical Environment)≠ 全局对象
示例:
var varVar = 1;
let letVar = 2;
const constVar = 3;
console.log(window.varVar); // 1 ✅
console.log(window.letVar); // undefined ❌
console.log(window.constVar); // undefined ❌引擎内部实现(简化):
GlobalExecutionContext = {
VariableEnvironment: globalThis, // var 存储在这里
LexicalEnvironment: { // let/const 存储在这里
letVar: <uninitialized>, // TDZ
constVar: <uninitialized>, // TDZ
outer: null
}
};
// var 声明
globalThis.varVar = 1; // 直接添加到全局对象
// let/const 声明
LexicalEnvironment.letVar = 2; // 存储在词法环境,不添加到全局对象3. 词法环境(Lexical Environment)
3.1 词法环境的定义
定义:词法环境(Lexical Environment)是 ECMAScript 规范中定义的环境记录(Environment Record),用于存储标识符绑定。
结构:
LexicalEnvironment {
EnvironmentRecord: {...}, // 环境记录
OuterLexicalEnvironment: reference // 外部词法环境引用
}
3.2 环境记录(Environment Record)
类型:
-
声明式环境记录(Declarative Environment Record)
- 存储
let、const、class、module等声明 - 存储函数声明(ES6+)
- 存储
catch子句中的变量
- 存储
-
对象环境记录(Object Environment Record)
- 将环境记录与对象关联
- 用于
with语句和全局对象
-
函数环境记录(Function Environment Record)
- 函数执行上下文使用
- 包含
this绑定、arguments对象
-
模块环境记录(Module Environment Record)
- ES6 模块使用
- 支持
import/export
3.3 词法环境的创建过程
3.3.1 全局词法环境
创建过程:
// 全局代码
let x = 10;
const y = 20;引擎内部实现(简化):
// 全局执行上下文创建
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
x: <uninitialized>, // TDZ
y: <uninitialized> // TDZ
},
OuterLexicalEnvironment: null // 全局词法环境没有外部引用
},
VariableEnvironment: globalThis
};
// 执行阶段
LexicalEnvironment.EnvironmentRecord.x = 10;
LexicalEnvironment.EnvironmentRecord.y = 20;3.3.2 函数词法环境
创建过程:
function test(a, b) {
let x = 10;
const y = 20;
var z = 30;
}引擎内部实现(简化):
// 函数执行上下文创建
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
// 函数环境记录
ThisBinding: <function>,
Arguments: { a: 1, b: 2 },
x: <uninitialized>, // let 声明,TDZ
y: <uninitialized> // const 声明,TDZ
},
OuterLexicalEnvironment: <GlobalLexicalEnvironment> // 外部词法环境
},
VariableEnvironment: {
z: undefined // var 声明,初始化为 undefined
}
};
// 执行阶段
LexicalEnvironment.EnvironmentRecord.x = 10;
LexicalEnvironment.EnvironmentRecord.y = 20;
VariableEnvironment.z = 30;3.4 块级词法环境
块级作用域的实现:
{
let blockVar = 10;
const blockConst = 20;
}引擎内部实现(简化):
// 块级作用域创建新的词法环境
BlockLexicalEnvironment = {
EnvironmentRecord: {
blockVar: <uninitialized>,
blockConst: <uninitialized>
},
OuterLexicalEnvironment: <父级词法环境>
};
// 执行后,块级词法环境可能被销毁(取决于闭包)4. 变量环境 vs 词法环境
4.1 区别对比
| 特性 | 变量环境(Variable Environment) | 词法环境(Lexical Environment) |
|---|---|---|
| 存储内容 | var 声明、function 声明(ES5) | let、const、class、function(ES6+) |
| 初始化 | undefined | <uninitialized>(TDZ) |
| 作用域 | 函数作用域/全局作用域 | 块级作用域 |
| 提升行为 | 提升并初始化为 undefined | 提升但进入 TDZ |
| 全局对象 | var 会添加到全局对象 | let/const 不会添加到全局对象 |
4.2 为什么需要两个环境
历史原因:
- ES5 之前只有
var和function,使用变量环境 - ES6 引入
let/const,需要新的环境记录机制 - 为了向后兼容,保留变量环境,新增词法环境
技术原因:
- 变量环境用于处理函数作用域的变量
- 词法环境用于处理块级作用域的变量
- 两者分离可以更精确地控制变量的生命周期
5. 执行上下文的生命周期
5.1 创建阶段(Creation Phase)
步骤:
-
创建词法环境(Lexical Environment)
LexicalEnvironment = { EnvironmentRecord: {...}, OuterLexicalEnvironment: <outer reference> }; -
创建变量环境(Variable Environment)
VariableEnvironment = { // 对于全局执行上下文,等于全局对象 // 对于函数执行上下文,创建新的环境记录 }; -
确定 this 绑定(This Binding)
ThisBinding = <determined by call>; -
处理声明(Declaration Processing)
var声明:在变量环境中创建,初始化为undefinedfunction声明:在变量环境中创建,初始化为函数对象let/const声明:在词法环境中创建,进入 TDZ
5.2 执行阶段(Execution Phase)
步骤:
-
变量赋值
// 在变量环境或词法环境中查找变量 // 执行赋值操作 -
代码执行
// 逐行执行代码 // 处理函数调用、表达式求值等
5.3 销毁阶段(Destruction Phase)
步骤:
-
判断是否可以销毁
- 如果没有闭包引用,可以销毁
- 如果有闭包引用,保留词法环境
-
内存回收
- 标记为可回收
- 等待垃圾回收器回收
6. 实际示例分析
6.1 全局执行上下文分析
代码:
var globalVar = 10;
let globalLet = 20;
const globalConst = 30;
function test() {
console.log(globalVar, globalLet, globalConst);
}执行上下文结构:
GlobalExecutionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
globalLet: 20,
globalConst: 30,
test: <function object>
},
OuterLexicalEnvironment: null
},
// 变量环境 = 全局对象
VariableEnvironment: globalThis, // 即 window (浏览器) 或 global (Node.js)
// globalThis.globalVar = 10 (var 声明在这里)
ThisBinding: globalThis
};验证:
console.log(window.globalVar); // 10 ✅
console.log(window.globalLet); // undefined ❌
console.log(window.globalConst); // undefined ❌6.2 函数执行上下文分析
代码:
function outer(a, b) {
var varVar = 1;
let letVar = 2;
const constVar = 3;
function inner() {
console.log(varVar, letVar, constVar);
}
return inner;
}
let func = outer(10, 20);
func();执行上下文创建过程:
1. outer 函数执行上下文创建:
FunctionExecutionContext(outer) = {
LexicalEnvironment: {
EnvironmentRecord: {
ThisBinding: undefined,
Arguments: { a: 10, b: 20 },
letVar: <uninitialized>,
constVar: <uninitialized>,
inner: <function object>
},
OuterLexicalEnvironment: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
varVar: undefined,
inner: <function object>
}
};2. outer 函数执行:
// 执行阶段
VariableEnvironment.varVar = 1;
LexicalEnvironment.EnvironmentRecord.letVar = 2;
LexicalEnvironment.EnvironmentRecord.constVar = 3;3. inner 函数执行上下文创建(闭包):
FunctionExecutionContext(inner) = {
LexicalEnvironment: {
EnvironmentRecord: {
ThisBinding: undefined,
Arguments: {}
},
OuterLexicalEnvironment: <outer的LexicalEnvironment> // 保留引用!
},
VariableEnvironment: {
// 空
}
};关键:inner 的外部词法环境引用指向 outer 的词法环境,形成闭包!
7. 变量提升(Hoisting)
7.1 什么是变量提升
定义:变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的”行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
重要说明:
- 变量和函数声明在代码里的位置是不会改变的
- 变量提升实际上是在编译阶段被 JavaScript 引擎放入内存中
- 对,你没听错,一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译
7.2 变量提升的示例
代码:
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数showName被执行')
}执行结果:
函数showName被执行
undefined
分析:
- 第1行输出”函数showName被执行”,说明函数可以在定义之前使用
- 第2行输出”undefined”,说明变量可以在定义之前使用,但值是
undefined
7.3 变量提升的底层机制
编译阶段:
// 原始代码
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数showName被执行')
}
// 编译阶段处理后的结果
// 第一部分:变量提升部分
var myname = undefined
function showName() {
console.log('函数showName被执行')
}
// 第二部分:可执行代码部分
showName()
console.log(myname)
myname = '极客时间' // 去掉 var 声明,保留赋值语句执行上下文创建:
// 编译阶段生成执行上下文
ExecutionContext = {
VariableEnvironment: {
myname: undefined, // var 声明,初始化为 undefined
showName: <function object> // function 声明,初始化为函数对象
},
LexicalEnvironment: {...},
ThisBinding: window
}执行阶段:
// 1. 执行 showName()
// JavaScript 引擎在 VariableEnvironment 中查找 showName
// 找到函数对象,执行函数,输出 "函数showName被执行"
// 2. 执行 console.log(myname)
// JavaScript 引擎在 VariableEnvironment 中查找 myname
// 找到 myname,值为 undefined,输出 undefined
// 3. 执行 myname = '极客时间'
// 在 VariableEnvironment 中查找 myname,赋值为 '极客时间'7.4 var 的提升机制
代码:
console.log(x);
var x = 10;编译阶段:
// 创建执行上下文
ExecutionContext = {
VariableEnvironment: {
x: undefined // var 声明,初始化为 undefined
}
}执行阶段:
// 1. console.log(x)
// 在 VariableEnvironment 中查找 x,值为 undefined
console.log(VariableEnvironment.x); // undefined
// 2. x = 10
// 在 VariableEnvironment 中查找 x,赋值为 10
VariableEnvironment.x = 10;7.5 let/const 的 TDZ(暂时性死区)
代码:
console.log(y);
let y = 20;编译阶段:
// 创建执行上下文
ExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
y: <uninitialized> // let 声明,进入 TDZ
},
OuterLexicalEnvironment: null
},
VariableEnvironment: {...}
}执行阶段:
// 1. console.log(y)
// 在 LexicalEnvironment 中查找 y
// y 处于 <uninitialized> 状态,仍在 TDZ 中
// ❌ ReferenceError: Cannot access 'y' before initializationTDZ 的特点:
let/const声明也会被提升,但不会初始化为undefined- 在声明之前访问变量会抛出
ReferenceError - 这被称为”暂时性死区”(Temporal Dead Zone,简称 TDZ)
7.6 函数提升
代码:
showName()
function showName() {
console.log('函数showName被执行')
}执行结果:
函数showName被执行
分析:
- 函数声明会被提升到代码开头
- 函数声明会被初始化为函数对象
- 可以在函数声明之前调用函数
编译阶段:
// 创建执行上下文
ExecutionContext = {
VariableEnvironment: {
showName: <function object> // function 声明,初始化为函数对象
}
}7.7 相同名称的变量和函数
代码:
function showName() {
console.log('极客邦')
}
showName()
function showName() {
console.log('极客时间')
}
showName()执行结果:
极客时间
极客时间
分析:
- 在编译阶段,遇到第一个
showName函数,会将该函数体存放到变量环境中 - 接下来是第二个
showName函数,继续存放至变量环境中 - 但是变量环境中已经存在一个
showName函数了,此时,第二个showName函数会将第一个showName函数覆盖掉 - 这样变量环境中就只存在第二个
showName函数了
结论:一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。
7.8 函数表达式不会被提升
代码:
showName()
var showName = function() {
console.log(2)
}
function showName() {
console.log(1)
}执行结果:
1
分析:
var showName会被提升,初始化为undefinedfunction showName()会被提升,初始化为函数对象- 函数声明会覆盖变量声明(但变量仍然存在)
- 执行
showName()时,调用的是函数声明 - 执行
showName = function() {...}时,会覆盖函数声明
编译阶段:
// 创建执行上下文
ExecutionContext = {
VariableEnvironment: {
showName: <function object> // function 声明优先
}
}执行阶段:
// 1. showName() 输出 1
// 2. showName = function() {...} 覆盖函数声明
// 如果后面再有 showName 执行的话,就输出 2,因为这时候函数引用已经变了8. 参考资源
- ECMAScript 262 - Execution Contexts
- ECMAScript 262 - Lexical Environments
- ECMAScript 262 - Environment Records
- V8 引擎博客