V8 引擎是怎么执行 JavaScript 的?

深入原理约 6 分钟读完

一句话回答

V8 先把源码解析成 AST,暂时不执行的函数只做预解析;然后解释器 Ignition 把 AST 转成字节码并解释执行,同时收集类型反馈;执行频繁的热点代码再交给优化编译器编译成机器码,这就是即时编译(JIT)。优化建立在对类型的假设上,假设被打破就去优化,退回字节码执行。

详细解析

整体流程

文本
源码
→ 解析:词法分析得到 token,语法分析得到 AST(暂不执行的函数只预解析)
→ Ignition:AST 转成字节码,解释执行,并收集类型反馈
→ 热点代码:按热度逐级交给 Sparkplug、Maglev、TurboFan 编译成机器码
→ 类型假设被打破:去优化,回到字节码执行

解析与惰性解析

词法分析把源码切成 token(关键字、标识符、运算符等),语法分析按语法规则把 token 组织成 AST,同时确定作用域。

页面里很多函数可能根本不会执行,全部完整解析既费时间又占内存。所以 V8 采用惰性解析:暂时不调用的函数只做预解析,检查语法错误、分析它引用了哪些外部变量(决定哪些变量要给闭包保留),但不生成 AST 和字节码;等函数真正被调用时再完整解析和编译。

多层编译

层级 角色 特点
Ignition 解释器 生成字节码,启动快、占内存少,执行时收集类型反馈
Sparkplug 基线编译器 把字节码直接翻译成机器码,不做优化,编译非常快
Maglev 中间层优化编译器 利用类型反馈做优化,在编译速度和代码质量之间折中
TurboFan 顶层优化编译器 做内联、逃逸分析等激进优化,代码最快,编译也最耗时

代码执行得越频繁,就会被交给越高的层级。从 Sparkplug 到 TurboFan,编译越来越慢,生成的代码也越来越快,多层编译就是在"编译开销"和"执行速度"之间找平衡。

去优化

优化编译器根据类型反馈做出假设,比如"参数总是整数""对象总是某种结构",生成专门的机器码,并在里面插入检查。检查失败就去优化(deoptimization):放弃优化代码,回到字节码执行,之后可能根据新的反馈重新优化。

JavaScript
function add(a, b) {
  return a + b
}

for (let i = 0; i < 100000; i++) add(i, 1) // 一直传整数:成为热点,被编译成针对整数的优化代码
add('1', '2') // 传入字符串:类型假设被打破,触发去优化

用 node --trace-opt --trace-deopt 运行,可以看到 add 先被优化,传入字符串时又被去优化。

隐藏类和内联缓存

JS 对象可以随时增删属性,如果每次访问属性都按名字查找,会很慢。V8 给每个对象关联一个隐藏类(V8 内部叫 Map),记录对象有哪些属性、每个属性存放在什么位置。从同一个起点出发(比如同一个构造函数),按相同的顺序添加相同的属性,得到的对象共享同一个隐藏类。比如字面量 { x: 1, y: 2 } 和 new Point(1, 2) 的属性名、顺序都一样,隐藏类却不同。

JavaScript
function Point(x, y) {
  this.x = x
  this.y = y
}
const p1 = new Point(1, 2)
const p2 = new Point(3, 4) // 结构相同,共享同一个隐藏类

const a = { x: 1, y: 2 }
const b = { y: 2, x: 1 } // 属性顺序不同,隐藏类不同

内联缓存(Inline Cache):在每个属性访问的位置缓存"隐藏类 → 属性位置",下次遇到同一个隐藏类,直接按位置读取,不用再查找。一个位置只遇到过一种隐藏类叫单态,最快;遇到 2~4 种叫多态;超过 4 种就成了超态,缓存基本失效。

写出对引擎友好的代码

  • 在构造函数中按相同的顺序一次性初始化所有属性,不要之后再动态添加
  • 避免 delete 属性,它可能让对象退化成"字典模式",属性访问变慢;不需要的属性可以设为 null
  • 数组元素的类型保持一致:V8 会按元素类型选择存储方式,混入其他类型后会转成更通用的方式,而且不会再转回来;也不要制造空洞
  • 函数参数的类型尽量稳定,单态比多态更容易优化

另外,V8 会缓存编译结果(代码缓存),同一个脚本再次加载时可以直接使用,跳过解析和编译,加快执行。

面试官可能追问

什么是 JIT?为什么不一开始就把所有代码编译成机器码?

JIT(Just-In-Time,即时编译)是在程序运行过程中,根据运行情况把代码编译成机器码。不一开始全部编译,是因为编译本身耗时,机器码也比字节码占用更多内存,而网页里大部分代码只执行一两次,编译的开销收不回来。先用解释器快速启动,只优化真正的热点代码,还能利用运行时收集到的类型信息做更激进的优化。

为什么说"JS 是解释型语言"不完全准确?

语言规范并没有规定 JS 必须解释执行还是编译执行,这取决于引擎的实现。现代引擎都是"解释 + 即时编译"的混合模式:先把源码编译成字节码(这本身就是一次编译),再解释执行,热点代码还会被编译成机器码。更准确的说法是:JS 是动态类型语言,现代引擎用 JIT 的方式执行它。

哪些操作会破坏隐藏类?
  • 在构造函数之外动态添加属性,或者不同对象以不同的顺序添加属性
  • delete 属性,对象可能退化成字典模式
  • 结果是访问这些对象的代码变成多态甚至超态,内联缓存失效,优化编译器也难以优化

易错点

  • V8 不是把 JS 直接编译成机器码:先生成字节码,只有热点代码才会被编译成机器码
  • 隐藏类相同,要求属性名和添加顺序都相同
  • 这些都是引擎的实现细节,会随版本变化;写代码以可读性为先,只在性能热点上注意这些规则

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。