V8 如何执行你的代码——编译、上下文与调用栈

来看一段代码,你猜输出什么?

js 复制代码
showName('极客时间');
console.log(myname);

var myname = '张三';
function showName(name) {
  var b = 1;
  console.log('函数showName 执行');
}

如果你觉得第一行就会报错------能理解,但错了。这段代码正常运行,输出 '函数showName 执行' 然后 undefined

这不是魔法。这是 JS 执行机制的外在表现。

第一层:变量提升只是表象

大多数教程这样解释:varfunction 声明会被"提升"到作用域顶部。所以上面那段代码在 V8 眼里等价于:

js 复制代码
var myname;                   // 变量提升
function showName() {         // 函数提升
  var b;
  b = 1;
  console.log('函数showName 执行');
}

showName();
console.log(myname);          // undefined
myname = '张三';

但"提升"只是一个比喻。代码没有被物理移动------任何一个 JS 引擎都不会改动你的源码。 真正发生的事情,在你看不到的地方。

第二层:编译总在执行前一刻发生

JS 不是纯解释型语言。V8 的执行流水线是:

编译阶段做四件事:

  1. 创建执行上下文对象
  2. 找形参和变量声明(var),将它们作为 key 存入变量环境,值初始化为 undefined
  3. 统一形参和实参的值(全局作用域没有这一步)
  4. 找函数声明,将函数名作为 key,值为函数体,存入变量环境

注意这个顺序。第 4 步会覆盖 第 2 步中同名 key 的值------这就是为什么函数声明优先级高于 var

js 复制代码
console.log(func);   // ƒ func() {}
function func() {}
var func = '123';

输出的是函数体,不是 undefined。因为编译阶段,函数声明的处理晚于 var,它覆盖了 undefined

所以呢? "变量提升"不是行为本身,而是编译阶段的编译产物。真正需要理解的概念是执行上下文

第三层:执行上下文里到底有什么

执行上下文不是一个抽象概念,它有明确的结构:

javascript 复制代码
(image)
执行上下文
├── 变量环境 (Variable Environment)
│   └── function, var 声明的变量存在这里
├── 词法环境 (Lexical Environment)
│   └── let, const 声明的变量存在这里
└── 待执行代码
    └── 从上到下,顺序执行

看这个例子:

js 复制代码
function varTest() {
  var x = 1;          // 变量环境
  if (true) {
    let x = 2;        // 词法环境(块级作用域)
    console.log(x);   // 2
  }
  console.log(x);     // 1
}
varTest();

编译完成后,var x 在变量环境里,值为 undefined。执行到 var x = 1 时赋值。

进入 if 块时,词法环境里压入一个新栈帧,存放 let x = 2。块内的 console.log(x) 查找变量时,先从词法环境栈顶往下找,找不到再去变量环境 。找到 x = 2,输出 2。

离开 if 块,该栈帧弹出。最后的 console.log(x) 在词法环境中找不到 x,去变量环境找,输出 1。

这就是 let/const 块级作用域的实现原理------词法环境是一个栈结构,进出块就是在压栈弹栈。

根因:一张图画完全貌

来看一个更复杂的例子,把所有概念串起来:

js 复制代码
function foo() {
  var a = 1;
  let b = 2;
  {
    let b = 3;
    var c = 4;
    let d = 5;
    console.log(a);  // 1
    console.log(b);  // 3
  }
  console.log(b);    // 2
  console.log(c);    // 4
  console.log(d);    // ReferenceError: d is not defined
}
foo();

执行过程逐帧拆解:

进入 foo 函数体的编译阶段:

  • 变量环境: { a: undefined, c: undefined } --- 注意 cvar,不受块限制,直接提到函数作用域
  • 词法环境: [ { b: <uninitialized> } ] --- let b 在函数作用域,处于 TDZ

执行 var a = 1; let b = 2; 后:

  • 变量环境: { a: 1, c: undefined }
  • 词法环境: [ { b: 2 } ]

进入块,词法环境压入新栈帧(编译块内声明,尚未执行赋值):

  • 变量环境: { a: 1, c: undefined } --- cvar,早已在函数作用域
  • 词法环境: [ { b: <uninitialized>, d: <uninitialized> }, { b: 2 } ] --- 块级 let 进入 TDZ

逐行执行块内代码:

  • let b = 3 → 词法环境栈顶的 b 赋值为 3
  • var c = 4 → 变量环境的 c 赋值为 4
  • let d = 5 → 词法环境栈顶的 d 赋值为 5

三条赋值语句执行完毕后:

  • 变量环境: { a: 1, c: 4 }
  • 词法环境: [ { b: 3, d: 5 }, { b: 2 } ]

接着 console.log(b) 从栈顶查找,找到 b = 3console.log(a) 在词法环境中找不到,去变量环境找,找到 a = 1

离开块后,栈顶弹出:

  • 变量环境: { a: 1, c: 4 } --- c = 4 在块内执行,但因为它是 var,存在于变量环境
  • 词法环境: [ { b: 2 } ] --- 块级 bd 随栈帧一起消失

最后的 console.log(d) 在词法环境和变量环境中都找不到,抛出 ReferenceError

调用栈:这一切的容器

调用栈是 V8 管理函数调用关系的数据结构。栈顶指针指向当前正在执行的函数(或全局)。

erlang 复制代码
调用栈
│
├── 全局执行上下文  ← 永远在栈底
├── foo 执行上下文
├── bar 执行上下文  ← 栈顶(当前执行)
...

规则很清晰:

  • 编译总在执行前一刻发生------全局和每个函数体都是独立编译的
  • 每个函数编译后生成一个执行上下文,压入调用栈
  • 函数执行完毕,它的执行上下文弹出并销毁

补充:为什么 let/const 有"暂时性死区"

letconst 在编译阶段也会被处理------它们进入词法环境,但不会被初始化为 undefined 。从进入作用域到实际执行声明语句之间的区域,变量处于 "未初始化" 状态,任何访问都会抛出 ReferenceError。这就是暂时性死区(Temporal Dead Zone)。

加上不可重复声明------这两条规则本质上都是在修补 var 的历史设计缺陷。理解了编译过程,这些"新规则"就不再需要死记硬背。

总结

回到开头那句话:JS 不是逐行解释执行的。它在执行前总有一道编译工序。 这道工序的产物就是执行上下文------变量环境管 var/function,词法环境管 let/const,词法环境的栈结构就是块级作用域的实现。

下次遇到变量行为的面试题,不用回忆"那道题的答案是什么"------画一下执行上下文的结构图,答案自己会走出来。

相关推荐
To_OC9 小时前
LC 994 腐烂的橘子:人人都说是 BFS 入门题,我却写了三遍才过
javascript·算法·leetcode
To_OC15 小时前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
labixiong17 小时前
实现一个能跑的迷你版Promise(一)
前端·javascript·面试
weedsfly1 天前
还在用 Axios?你可能需要重新理解 XHR 与 Fetch
前端·javascript·面试
CoderWeen1 天前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
To_OC1 天前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
kyriewen2 天前
我用 50 行代码重写了 React Router 核心,终于搞懂了前端路由原理
前端·javascript·react.js
Asize2 天前
HTML5 Canvas 基础:从按帧动画到 ECharts 数据可视化
前端·javascript·canvas
默_笙2 天前
🎄 后端给我一堆扁平数据,我 10 行代码把它变成了树
前端·javascript
前端Hardy2 天前
又一个 AI 神器火了!
前端·javascript·后端