为什么执行上下文是 JavaScript 运行时的核心
当你写下一段 JavaScript 代码并让它运行,JavaScript 引擎并不是逐行解析执行那么简单。在代码真正执行之前,引擎会先创建一个执行上下文(Execution Context),这是代码运行时的环境容器。理解执行上下文,才能理解变量提升、作用域链、闭包、this 绑定的底层机制。
这篇文章会让你看到 JavaScript 引擎在执行代码之前做了什么,以及为什么某些看似奇怪的行为(如 var 变量提升、let 的暂时性死区)其实是引擎设计的有意之举。
一、执行上下文的本质
1.1 什么是执行上下文
执行上下文是 JavaScript 引擎管理代码执行的一种机制。每个执行上下文包含:
- 变量环境(VariableEnvironment):存储
var声明和函数声明 - 词法环境(LexicalEnvironment):存储
let、const、模块作用域 - this 绑定(ThisBinding):决定
this关键字的值 - 外部环境引用(Outer Environment Reference):形成作用域链
1.2 三种执行上下文
flowchart TD
A["执行上下文栈<br/>Call Stack"] --> B["全局执行上下文<br/>Global EC"]
B --> C["函数执行上下文<br/>Function EC"]
C --> D["Eval 执行上下文<br/>Eval EC"]
全局执行上下文:代码首次运行时的默认上下文。在浏览器中,全局上下文关联到 window 对象;在 Node.js 中,关联到 global 对象。
函数执行上下文:每次调用函数时创建。函数不直接创建上下文——调用时才创建。
Eval 执行上下文:不推荐使用,eval 代码在特殊上下文中运行。
二、执行上下文栈(Call Stack)
2.1 栈的工作原理
JavaScript 是单线程语言,通过栈结构管理执行上下文的嵌套关系:
function first() {
second();
console.log("first");
}
function second() {
third();
console.log("second");
}
function third() {
console.log("third");
}
first();
执行顺序分析:
栈状态变化:
1. first() 被调用
Stack: [Global EC, first() EC]
2. first() 内部调用 second()
Stack: [Global EC, first() EC, second() EC]
3. second() 内部调用 third()
Stack: [Global EC, first() EC, second() EC, third() EC]
4. third() 执行 console.log("third"),然后返回
Stack: [Global EC, first() EC, second() EC]
5. second() 执行 console.log("second"),然后返回
Stack: [Global EC, first() EC]
6. first() 执行 console.log("first"),然后返回
Stack: [Global EC]
输出顺序:third → second → first
2.2 栈溢出(Stack Overflow)
递归调用如果没有终止条件,会不断压栈直到超出限制:
function infinite() {
infinite();
}
infinite(); // RangeError: Maximum call stack size exceeded
现代浏览器和 Node.js 的栈大小大约在 1-2 万层左右。
三、变量环境 vs 词法环境
3.1 两者对比
| 特性 | 变量环境(VariableEnvironment) | 词法环境(LexicalEnvironment) |
|---|---|---|
| 创建时机 | 函数调用时 | 函数/模块加载时 |
| 存储内容 | var 声明、函数声明 |
let、const、模块作用域 |
| 提升行为 | 完全提升(声明和赋值都提升) | 仅声明提升,赋值留在原地 |
| 块级作用域 | 不支持 | 支持({} 块) |
3.2 ECMAScript 规范定义
根据 ECMA-262 规范,执行上下文由以下组件构成:
ExecutionContext = {
VariableEnvironment: { // var 和函数声明
envRecord: { ... },
outer: <outer reference>
},
LexicalEnvironment: { // let, const, 模块作用域
envRecord: { ... },
outer: <outer reference>
},
ThisBinding: <this value>
}
3.3 代码验证
function test() {
console.log(a); // undefined — var 提升
console.log(b); // ReferenceError — let 的 TDZ
var a = 1;
let b = 2;
}
test();
四、作用域链(Scope Chain)
4.1 作用域链的形成
当代码访问变量时,引擎沿作用域链从内向外查找:
const global = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(innerVar); // 自身
console.log(outerVar); // 从 outer 作用域找到
console.log(global); // 从全局作用域找到
}
inner();
}
outer();
flowchart TD
subgraph Global["全局作用域"]
G["global = 'global'"]
end
subgraph outer_EC["outer 执行上下文"]
O["outerVar = 'outer'"]
end
subgraph inner_EC["inner 执行上下文"]
I["innerVar = 'inner'"]
end
I -->|outer| O
O -->|outer| G
4.2 外部环境引用的设定时机
外部环境引用在函数定义时确定,而非调用时:
function createAdder(x) {
return function(y) {
return x + y; // x 从 createAdder 的作用域捕获
};
}
const add5 = createAdder(5);
console.log(add5(10)); // 15
createAdder 执行完毕后,其执行上下文应该弹出栈,但因为内部函数仍然引用 x,所以 createAdder 的执行上下文不会被完全销毁——这就是闭包的雏形。
五、闭包的本质
5.1 闭包的定义
闭包是指函数能够记住并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。
5.2 闭包的实现机制
当函数被创建时,函数有一个内部属性 [[Environment]](规范中的内部槽),指向创建时的词法环境。当函数执行时,新的执行上下文的外部环境引用被设置为函数的 [[Environment]]。
function outer() {
const x = 10;
function inner() {
console.log(x); // 访问 outer 中的 x
}
return inner;
}
const fn = outer();
fn(); // 10 — outer 的执行上下文虽然已弹出栈,
// 但 inner 的 [[Environment]] 仍然引用着它
5.3 V8 的优化:上下文分配
V8 会分析变量是否被内部函数引用。只有被引用的变量才会被分配到堆上的 Context 对象中,未被引用的变量仍然在栈上,函数结束后自动销毁。
function optimized() {
const used = 1; // 被闭包引用 → 分配到堆
const unused = 2; // 未被引用 → 栈上分配,执行后销毁
return function() {
return used;
};
}
六、面试高频问题
Q: 什么是执行上下文?
执行上下文是 JavaScript 引擎管理代码执行的抽象概念。每次代码运行(全局代码、函数调用、eval)都会创建一个执行上下文。执行上下文包含变量环境、词法环境、this 绑定和外部环境引用。JavaScript 通过执行上下文栈管理多个上下文的嵌套关系。
Q: 变量提升的原理是什么?
变量提升源于 JavaScript 引擎的两阶段处理:第一阶段是创建阶段,引擎扫描代码并将 var 声明和函数声明加入变量环境;第二阶段是执行阶段,按照代码顺序执行。var 的声明和赋值都会被提升,但赋值留在原地,所以提升后访问是 undefined。函数声明整体提升,可以在声明前调用。
Q: 闭包是如何形成的?
闭包的形成需要三个条件:函数嵌套、内部函数引用外部变量、内部函数在外部函数执行完毕后仍可达。当函数创建时,其 [[Environment]] 内部槽记录创建时的词法环境。只要函数对象存在,这个词法环境就不会被垃圾回收,即使创建它的外部函数已经执行完毕。