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,词法环境的栈结构就是块级作用域的实现。

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

相关推荐
Aphasia3112 小时前
从内存模型看深浅拷贝
前端·javascript·面试
云水一下2 小时前
TypeScript 从零基础到精通(二):基础类型与类型系统
javascript·typescript
你怎么知道我是队长2 小时前
CRC校验C语言实现-CRC8、CRC16、CRC16的直接计算法、查表法
c语言·前端·javascript
meilindehuzi_a3 小时前
深入理解 JavaScript 执行机制:从编译阶段到调用栈底层实现
开发语言·javascript·ecmascript
小雨下雨的雨3 小时前
基于鸿蒙PC Electron框架技术完成的表单验证技术详解
前端·javascript·华为·electron·前端框架·鸿蒙
提子拌饭1333 小时前
饮料含糖量查询应用 - 鸿蒙PC用Electron框架完整实现
前端·javascript·华为·electron·前端框架·鸿蒙
hsg773 小时前
简述:Jensen Huang‘s Footsteps网站全内容分析
前端·javascript·数据库
大家的林语冰4 小时前
Angular 王者归来,第 22 个主版本亮相,一大波前沿技术再度引领潮流!
前端·javascript·前端框架
老毛肚4 小时前
jeecgboot TS + Vue 模板化 03
前端·javascript·vue.js