一、执行上下文的本质定义
执行上下文(Execution Context, EC) 是 ECMAScript 规范中定义的抽象概念 ,代表一段可执行代码被评估和执行时的完整环境状态。
从实现角度看,它是引擎在内存中维护的一个结构化数据对象,包含执行所需的所有绑定信息。
二、三种执行上下文的精确分类
| 类型 | 创建时机 | 数量 | 销毁时机 |
|---|---|---|---|
| 全局执行上下文(GEC) | 脚本启动时 | 且仅有1个 | 页面关闭/进程终止 |
| 函数执行上下文(FEC) | 每次函数调用时 | 无限制(按调用次数) | 函数返回(除闭包外) |
| Eval 执行上下文(EEC) | eval() 调用时 |
取决于调用次数 | 代码执行完毕 |
关键点:函数定义时不创建上下文,只有调用时才创建。
三、执行上下文的核心组件(规范视角)
根据 ECMAScript 规范,每个执行上下文都包含以下必需组件:
TypeScript
interface ExecutionContext {
// 1. 词法环境:处理 let/const 声明
LexicalEnvironment: EnvironmentRecord;
// 2. 变量环境:处理 var/function 声明
VariableEnvironment: EnvironmentRecord;
// 3. this 绑定:当前上下文中的 this 值
ThisBinding: any;
// 4. 额外的追踪信息(引擎实现相关)
// 例如:代码执行位置、回收标记等
}
3.1 环境记录(Environment Record)是什么?
环境记录是一个规范类型 ,本质上是一个键值存储结构,负责维护标识符绑定。
分为两种:
声明式环境记录:
-
存储变量、函数、参数
-
用于函数、模块、catch 块
对象式环境记录:
-
包装外部对象(如
window) -
用于全局作用域
四、生命周期:创建 → 执行 → 销毁
这是理解执行上下文最核心的部分。每个上下文都严格经历三个阶段。
第一阶段:创建阶段(Creation Phase)
此时代码一行都没执行,引擎完成三件事:
4.1 创建变量环境(处理 var/function)
javascript
// 示例代码
function test(param) {
var a = 1;
function b() {}
let c = 2;
const d = 3;
}
test(10);
创建阶段发生的事情(严格按顺序):
步骤1:函数参数初始化
- 创建参数绑定:param = 10
步骤2:函数声明提升
- 扫描函数声明 b
- 完整函数体存入环境
- 注意:如果已有同名参数,函数声明会覆盖
步骤3:变量声明提升(var)
- 扫描 var 声明 a
- 创建绑定 a = undefined
- 注意:不覆盖已存在的同名函数/参数
步骤4:let/const 处理
- 扫描 let/const 声明
- 创建绑定但标记为"未初始化"
- 进入"暂时性死区"
此时变量环境的结构:
javascript
VariableEnvironment = {
param: 10,
b: function, // 函数声明
a: undefined, // var 声明
// let/const 不在此处
}
4.2 创建词法环境(处理 let/const)
javascript
LexicalEnvironment = {
c: <uninitialized>, // 未初始化,不可访问
d: <uninitialized>
}
4.3 确定 this 绑定
ThisBinding 的确定规则(按优先级从高到低):
| 调用方式 | this 指向 |
|---|---|
new fn() |
新创建的对象 |
fn.call(obj) / fn.apply(obj) |
指定的 obj |
obj.fn() |
obj |
| 普通函数调用(非严格模式) | 全局对象(浏览器 window) |
| 普通函数调用(严格模式) | undefined |
| 箭头函数 | 继承外层 this |
| 事件处理器 | 绑定的 DOM 元素 |
第二阶段:执行阶段(Execution Phase)
代码开始逐行执行,引擎实时修改环境记录中的绑定值。
javascript
// 执行阶段逐行推进
var a = 1; // a 从 undefined → 1
let c = 2; // c 从未初始化 → 2(此时离开暂时性死区)
const d = 3; // d 从未初始化 → 3,不可再赋值
第三阶段:销毁阶段
一般情况下:函数返回后,执行上下文被标记为可回收,下次 GC 时回收内存。
特殊例外(闭包):如果内部函数持有外部函数变量的引用,外部函数的环境记录不会被销毁。
javascript
function outer() {
var secret = 'still here';
return function inner() {
console.log(secret); // 持有 secret 的引用
};
}
const fn = outer();
// outer 的上下文不会销毁,因为 secret 被 inner 引用
五、执行上下文栈(ECS):调用管理的核心机制
JS 引擎使用栈数据结构管理执行上下文,确保正确的执行顺序。
5.1 栈的底层行为
javascript
function a() { b(); }
function b() { c(); }
function c() { throw new Error('trace'); }
a();
抛出错误的堆栈信息(从底向上读):
bash
at c (repl:3:18)
at b (repl:2:13)
at a (repl:1:11)
at repl:1:1
对应的栈状态变化(压栈视角):
bash
初始:[全局EC]
调用 a(): [全局EC, aEC]
a 内调用 b():[全局EC, aEC, bEC]
b 内调用 c():[全局EC, aEC, bEC, cEC]
c 执行完毕:[全局EC, aEC, bEC] ← cEC 弹出并销毁
b 执行完毕:[全局EC, aEC] ← bEC 弹出并销毁
a 执行完毕:[全局EC] ← aEC 弹出并销毁
5.2 栈的最大深度限制
不同引擎的调用栈限制(约数):
| 引擎 | 最大调用深度 |
|---|---|
| V8 (Chrome/Node) | ~10,000 - 20,000 |
| SpiderMonkey (Firefox) | ~50,000+ |
| JavaScriptCore (Safari) | ~10,000 |
超过限制触发 RangeError: Maximum call stack size exceeded
六、作用域链的底层原理
很多人理解作用域链是"链条",但规范中的实现更精确。
6.1 词法环境嵌套结构
每个环境记录有一个 [[OuterEnv]] 引用,指向外层环境。
javascript
// 嵌套结构示意
GlobalEnvironment = {
records: { ... },
[[OuterEnv]]: null // 全局的外层是 null
}
FunctionEnvironment = {
records: { ... },
[[OuterEnv]]: GlobalEnvironment // 指向外层
}
InnerFunctionEnvironment = {
records: { ... },
[[OuterEnv]]: FunctionEnvironment // 指向外层
}
6.2 标识符解析算法
当引擎遇到变量 x 时,执行以下查找:
bash
1. 在当前环境记录中查找 'x'
2. 如果找到 → 返回绑定的值
3. 如果没找到 → 沿 [[OuterEnv]] 进入外层环境
4. 重复步骤 1-3
5. 直到 [[OuterEnv]] === null
6. 如果全局也找不到 → 抛出 ReferenceError
时间复杂度:理论上 O(作用域链深度),通常很小(< 10 层)。
七、VO/AO 的历史演变(ES3 → ES5+)
如果你看过老文章,会遇到 VO 和 AO 这两个术语。
ES3 时代
-
变量对象(VO):全局上下文中使用的环境
-
激活对象(AO):函数上下文中的变量对象,额外存储参数
-
两者本质相同,只是创建时机不同
ES5+ 时代(当前标准)
用 LexicalEnvironment 和 VariableEnvironment 替代 VO/AO,更精确地区分:
-
let/const→ 词法环境 -
var/function→ 变量环境
核心差异 :变量环境在创建阶段完全初始化,词法环境的 let/const 保持未初始化直到执行阶段。
八、完整代码的底层执行模拟
用这个示例把所有概念串起来:
bash
var globalVar = 'G';
let globalLet = 'GL';
function outer(y) {
var outerVar = 'O';
let outerLet = 'OL';
function inner(z) {
var innerVar = 'I';
return globalVar + y + outerVar + z + innerVar;
}
return inner(40);
}
outer(20);
分步执行记录
bash
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 0:创建全局执行上下文(创建阶段)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
变量环境:
globalVar: undefined
outer: function ← 完整函数体
词法环境:
globalLet: <uninitialized>
ThisBinding: globalThis
作用域链:
GlobalEnv.[[OuterEnv]] = null
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 0-执行:运行全局代码
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
globalVar = 'G' // 变量环境更新
globalLet = 'GL' // 词法环境更新(离开 TDZ)
outer 函数定义完成 // outer.[[Scope]] = GlobalEnv
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 1:调用 outer(20)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【创建阶段】
参数绑定:y = 20
变量环境:outerVar: undefined, inner: function
词法环境:outerLet: <uninitialized>
[[OuterEnv]]:指向 GlobalEnv
ThisBinding:globalThis
【执行阶段】
outerVar = 'O'
outerLet = 'OL'
inner 函数定义完成(inner.[[Scope]] = outer 的环境)
调用 inner(40):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 2:调用 inner(40)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【创建阶段】
参数绑定:z = 40
变量环境:innerVar: undefined
[[OuterEnv]]:指向 outer 的环境
【执行阶段】
innerVar = 'I'
查找 globalVar:inner 没有 → outer 没有 → GlobalEnv 有 → 'G'
查找 y:inner 没有 → outer 有 → 20
查找 outerVar:inner 没有 → outer 有 → 'O'
查找 z:inner 有 → 40
查找 innerVar:inner 有 → 'I'
返回拼接结果:'G' + 20 + 'O' + 40 + 'I'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 3-5:逐层返回并销毁
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
inner 返回 → innerEC 弹出销毁
outer 返回 → outerEC 弹出销毁(无闭包引用)
全局执行上下文持续存在
九、常见误区澄清
| 误区 | 真相 |
|---|---|
| 函数定义时就创建执行上下文 | 函数调用时才创建 |
| 上下文包含所有代码 | 只有可执行代码(全局、函数、eval)会触发创建 |
| 闭包保持整个上下文存活 | 只保持被引用的环境记录,不是完整上下文 |
this 在创建阶段就确定 |
对,且不会在执行阶段改变(箭头函数除外) |
| 变量提升是引擎"移动"了代码 | 不移动,只是创建阶段提前声明 |
十、一句话总结
执行上下文是 JS 引擎在运行全局代码或调用函数时创建的规范对象,包含词法环境(let/const)、变量环境(var/function)和 this 绑定,经历创建→执行→销毁三阶段,通过栈结构管理调用顺序。
把这段话拆解记住:
-
什么时候创建:脚本启动、函数调用时
-
包含什么:环境记录 + this 绑定
-
经历什么:创建(声明绑定)→ 执行(赋值运行)→ 销毁(回收)
-
如何管理:调用栈(压栈出栈)