深入理解:V8 的编译与执行机制

60 阅读6分钟

在很多前端开发者的印象里,JavaScript 是“解释执行”的脚本语言:代码从上往下执行即可。
但当你真正深入 V8 引擎,会发现这句话只对了一半。

JavaScript 在 执行之前,其实经历了一个极为关键的“编译阶段”——
这个阶段决定了 变量提升作用域函数声明优先级 等语言特性。
理解它,是写出无 bug、性能稳定、行为可预期代码的前提。

本文将带你从原理到实践,系统剖析 JS 的执行机制,最后配合 7 个经典样例逐行分析。
读完,你将彻底弄清楚:

  • 为什么 JS 代码“写在前不一定先执行”;
  • 为什么 varletconst 的表现差异如此之大;
  • 为什么某些变量是 undefined 而不是报错;
  • 为什么函数声明优先级最高;
  • 为什么对象和基本类型的赋值结果完全不同。

一、V8 引擎到底干了什么?

Chrome 使用的 V8 引擎 是目前最主流的 JS 引擎之一,它的任务是:

  1. 编译:将 JS 源码编译成更底层的可执行机器码。
  2. 执行:管理调用栈,执行代码逻辑,并分配/回收内存。

V8 在执行一段 JS 时,会划分出两个阶段:

1. 编译阶段

  • 检查语法错误(Syntax Error);
  • 变量提升(Hoisting);
  • 创建执行上下文(Execution Context);
  • 准备变量环境与词法环境。

2. 执行阶段

  • 正式运行代码;
  • 变量赋值;
  • 函数调用;
  • 上下文入栈/出栈;
  • 垃圾回收。

V8 并不是一次性编译整个脚本,而是边编译、边执行的流式执行模式。
每当遇到一个函数调用,都会重新经历“编译 → 执行”的过程。


二、执行上下文与调用栈

理解 JS 的执行流程,关键在于两个概念:

1. 执行上下文(Execution Context)

它是 JS 在执行代码时的“运行环境容器”,分为三类:

  • 全局执行上下文:程序一开始时创建;
  • 函数执行上下文:每当函数调用时创建;
  • Eval 执行上下文:少见,不建议使用。

每个执行上下文都包含三个重要部分:

  1. 变量环境(Variable Environment) :存储 var 声明的变量;
  2. 词法环境(Lexical Environment) :存储 letconst 声明的变量;
  3. this 绑定

2. 调用栈(Call Stack)

调用栈是 V8 内部维护的一个 后进先出(LIFO) 结构:

  • 全局上下文首先入栈;
  • 函数调用时,新的上下文被压入栈顶;
  • 函数执行完毕后出栈;
  • 栈空时程序结束。

这就是为什么“函数执行完后变量会被销毁”的根本原因。


三、编译阶段的完整流程

以一段伪代码为例:

function fn(x) {
    var y = 2;
    let z = 3;
}
fn(10);

当 V8 看到这段代码时,它会做如下几步:

  1. 全局编译阶段

    • 创建全局执行上下文;
    • 在变量环境中注册 fn
    • 暂不执行。
  2. 执行阶段

    • 执行 fn(10)

    • 创建新的函数执行上下文;

    • 编译 fn 函数体:

      • var y 放入变量环境(初始值为 undefined);
      • let z 放入词法环境(处于暂时性死区);
      • 形参 x 与实参 10 绑定。
  3. 函数执行

    • y = 2
    • z = 3
    • 函数执行完毕后,上下文出栈。

四、7 个经典样例逐行解析

例 1:函数声明与变量提升

showName();
console.log(myName);
console.log(hero);

var myName = 'zhangshan';
let hero = "钢铁侠";
function showName() {
    console.log("函数showName被执行");
}

输出:

函数showName被执行
undefined
ReferenceError: Cannot access 'hero' before initialization

解析:

  • showName() 函数声明在编译阶段就被提升;
  • var myName 被提升但值为 undefined
  • let hero 被提升但放入词法环境,在“暂时性死区”内;
  • 当访问 hero 时,因尚未初始化而抛出错误。

例 2:为什么要有变量提升?

var myName;
function showName() {
    console.log("函数showName被执行");
}
showName();
console.log(myName);
myName = "zhangshan";

输出:

函数showName被执行
undefined

解析:
编译阶段函数声明优先于变量声明,且 var 提升到顶层,初始化为 undefined
这使得我们在定义前调用函数或访问变量不会报错,而只是未赋值。


例 3:函数作用域与变量提升细节

var a = 1;
function fn(a) {
    console.log(a);
    var a = 2;
    var b = a;
    console.log(a);
}
fn(3);

输出:

3
2

解析:
编译阶段:

  • 参数 a 先于 var a 存在;

  • var a 提升但被参数同名遮蔽;

  • 执行时:

    • 第一次 console.log(a) → 输出参数 3;
    • a = 2 后,第二次输出 2

如果把 var a = 2; 改成 function a(){},函数声明优先,会导致不同结果,这正是“函数声明优先于变量声明”的体现。


例 4:var 与 let 的重复声明对比

console.log(a);
console.log(b);
var a = 1;
var a = 2;
console.log(a);
let b = 3;
console.log(b);

输出:

undefined
ReferenceError: Cannot access 'b' before initialization

解析:

  • var a 提升到顶层,初始化 undefined
  • 重复声明被忽略;
  • let b 提升但处于暂时性死区;
  • 访问 b 时抛出错误。

例 5:严格模式的不同表现

'use strict';
var a = 1;
var a = 2;

输出:

(正常执行,不报错)

在严格模式下,重复声明 var 仍然合法。
但如果是 letconst,则会直接报错。
严格模式更多的是防止隐式全局变量、禁止 this 指向 window、禁止重复参数名等。


例 6:函数表达式不会提升

func();
let func = () => {
    console.log('函数表达式不会提升');
}

输出:

ReferenceError: Cannot access 'func' before initialization

解析:
箭头函数本质上是一个函数表达式,只有在执行阶段才会赋值给变量。
编译阶段,func 只是被记录在词法环境中,但未初始化。
访问时仍处于暂时性死区,导致错误。


例 7:基本类型与引用类型的内存差异

let str = 'hello';
let str2 = str;
str2 = '你好';
console.log(str, str2);

let obj = { name: '张三', age: 18 };
let obj2 = obj;
obj2.age++;
console.log(obj2, obj);

输出:

hello 你好
{ name: '张三', age: 19 } { name: '张三', age: 19 }

解析:

  • 基本类型(Number、String、Boolean、Symbol、BigInt、Undefined、Null)存储在栈内存中,赋值时是值拷贝
  • 引用类型(Object、Array、Function)存储在堆内存中,赋值时是地址拷贝
  • 因此修改 obj2 同时会影响 obj

五、为什么 JS 要设计“边编译边执行”?

因为 JS 是浏览器脚本语言,必须即时响应用户操作
如果像 C++ 那样先整体编译再执行,网页响应速度会极慢。

于是,V8 采用了:

  • 解释执行 + JIT(即时编译) 的混合模式;
  • 在运行时动态优化热点代码;
  • 将高频函数编译为高性能机器码。

这种机制让 JS 能既保持灵活,又兼顾性能。


六、总结:JS 执行机制的“三层模型”

层级内容关键特性
编译阶段创建执行上下文、变量提升、函数提升var = undefined, let/const 暂时性死区
执行阶段从上到下执行语句,赋值、调用函数执行栈控制流程
内存层栈(基本类型)、堆(引用类型)值拷贝 vs 地址拷贝

再加上调用栈这一结构,JS 就具备了清晰、可预测的执行逻辑:

  1. 编译总是发生在执行前的一瞬间
  2. 全局与函数体都要创建自己的执行上下文
  3. 函数执行完毕后上下文出栈并销毁
  4. 变量提升、暂时性死区、作用域链 全都建立在这个机制之上。