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

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

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

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

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

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

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

大多数教程这样解释:var 和 function 声明会被"提升"到作用域顶部。所以上面那段代码在 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 } --- 注意 c 是 var,不受块限制,直接提到函数作用域
  • 词法环境: [ { b: <uninitialized> } ] --- let b 在函数作用域,处于 TDZ

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

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

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

  • 变量环境: { a: 1, c: undefined } --- c 是 var,早已在函数作用域
  • 词法环境: [ { 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 = 3。console.log(a) 在词法环境中找不到,去变量环境找,找到 a = 1。

离开块后,栈顶弹出:

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

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

调用栈:这一切的容器

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

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

规则很清晰:

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

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

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

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

总结

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

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

相关推荐
米米商聊9 小时前
同名附件为什么被 Map 覆盖?重复键与一对多索引
前端·javascript
kyriewen12 小时前
我把 10 道面试题答案真跑了一遍:6 道是错的
前端·javascript·面试
WebInfra13 小时前
Rspack 2.3 发布:联手 SWC 团队带来全新编译器
前端·javascript·github
FRing0314 小时前
JavaScript入门到精通三十一(原型继承、原型链)
开发语言·前端·javascript
程序员大阳14 小时前
副队长JavaScript教程(52)-DOM操作元素的内容
javascript·dom·innertext·innerhtml·value·内容操作
裕波16 小时前
苏千|从 cnpm 到 Vite+:AI 时代的开发者体验演进
javascript
console.log('npc')16 小时前
周期任务功能完整实现:CRON 编辑器 + 模板变量 + 执行历史
javascript·vue.js·编辑器
程序员大阳16 小时前
副队长JavaScript教程(58)-基于DOM的网页留言板开发
javascript·实例·dom·案例·留言板
咚咚咚小柒16 小时前
【前端】LangChain.js学习
前端·javascript·人工智能·typescript·langchain·vue·ai编程
程序员大阳18 小时前
副队长JavaScript教程(55)-DOM节点增删改查
javascript·dom·增删改查·节点·克隆