深入浅出 JS 编译

322 阅读6分钟

前言

一个老生常谈的问题,JS究竟是不是解释型语言,这个问题的本质其实还是得看执行 JS 的代码引擎究竟是如何处理 JS 代码的。如 V8引擎,SpiderMonkey,JSC 等,都有 JIT(just-in-time) Compiler,即将热点代码从字节码(Bytecode)进一步处理成机器码(Machine Code),提升代码执行速度,从这个角度看,JS 就不是常规意义上的解释型语言了,而是有了编译型语言的特点。

但是 JS 又与传统的编译性语言不同,它不是提前编译的,编译结果也不能在分布式系统中移植 —— 《你不知道的JavaScript》

严格来说,JS是即时编译性语言,因为编译后没有生成编译文件,而是马上执行了,和传统编译型语言有区别,说是解释性语言

JS 就是这么一个充满可能性的语言,接下来,我们就深入浅出一下 JS 编译原理。

什么是编译

Pasted image 20250715222959.png One language to another 将一种语言变成其他语言

  • A: Source 2 Source
    • 如 TS -> JS
    • JSX -> JS
    • ES6、ES7 语法降级 ES5
  • B: Source 2 Bytecode
    • 如 解释性语言
  • C: Source 2 MachineCode
    • 如 编译性语言
  • D: Bytecode 2 MachineCode
    • JIT

其中 Source 2 Source 也称之为预编译,通常指的是在开发阶段或构建阶段(Build Time),使用工具将一种形式的源代码转换为另一种形式的源代码的过程。这个过程也常被称为 “转译” (Transpiling) ,这个词其实更准确。

接下来的文章中,不明确指出的话,编译不包括预编译,即 A 的环节,因为这一步的执行者通常不是 JS 引擎,而是某些开发/构建工具 (例如 Babel, TypeScript Compiler (tsc), Webpack, Vite 等)。

JS代码编译大致流程

Pasted image 20250716153525.png

  1. 源代码 → 解析器 → 抽象语法树 (AST)
  2. AST → 解释器 (如 Ignition) → 字节码
  3. 解释执行字节码,同时监控器分析代码运行情况
  4. 识别热点代码 → JIT编译器 (如 TurboFan) → 优化后的机器码
  5. 如果优化假设失败 → 去优化 → 回退到字节码执行

Source Code -> AST

当JavaScript引擎获取到JS Source Code后,编译的第一步便是被解析器 parser 解析(Parsing)。这个阶段主要包含两个核心环节:

  • 词法分析(Lexical Analysis):此阶段会将您的源代码字符串分解成一个个有意义的单元,称为词法单元(Tokens),这个过程也被称为分词(Tokenization)
  • 语法分析(Syntax Analysis):在获得词法单元流后,语法分析器会根据JavaScript的语法规则,将这些词法单元组合成一个树形结构,即抽象语法树(Abstract Syntax Tree, AST)。AST是源代码语法结构的抽象表示,它精确地反映了代码的层次和关系。例如,一个变量声明语句在AST中会是一个节点,它包含了变量名和被赋予的值等信息。开发者可以利用像 AST Explorer 这样的工具来直观地查看代码生成的AST。

AST -> Bytecode

直接执行抽象语法树AST也是可以的,但是其由于其结构特点,导致执行效率低,所以通常要处理成 Bytecode字节码

  • AST 树是重型的数据结构,本身内存占用就比较高
  • 树结构,节点和节点之间并不是相邻的,内存不是相邻的,导致CPU 的 Cache 缓存命中率低
  • 树遍历的过程中有很多的节点跳转,需要很多的指令访问

AST 被字节码生成器(generator)、字节码优化器(opimizer)处理后,生成可以被字节码解释器(interpreter)解释执行的字节码。正所谓磨刀不误砍柴工,处理一下后 Bytecode 的执行效率有了较大提升。

采用字节码的好处在于:

  • 减少内存占用:相比于AST或者直接生成机器码,字节码更加紧凑,可以显著降低内存消耗。
  • 提升启动速度:生成字节码的速度远快于生成优化后的机器码,这使得代码可以更快地开始执行,优化了应用的启动性能。

字节码是一种与特定物理机器无关的中间代码,它比AST更接近机器码,但又比机器码更具平台无关性。可以将其理解为JavaScript虚拟机(VM)能够直接理解和执行的指令集。

字节码本质就是内存中的一条条数据,每条数据代表一条指令,去模拟 CPU执行指令

JS引擎在初次首先解释执行字节码(解释型执行)。在这个阶段,代码会逐行执行,但这个执行过程相对机器码直接执行而言仍然较慢。

ByteCode -> Machine Code

JS 引擎将 AST 处理成字节码后,还会监控相关代码的执行状态,识别出频繁执行的代码块(即所谓的 "热代码"),则会进一步将热点代码编译成 Machine Code机器码。这个过程称为 即时编译(JIT),编译后的机器码可以被直接执行,而不再需要解释。

某些代码段的执行模式已经固定,JS 引擎还会对其进行更多优化。例如,使用 内联缓存(Inline Caching) 等技术,提高访问属性或方法的速度。

Machine Code-> ByteCode

JIT编译器的优化是建立在一些假设(Assumptions) 之上的。例如,一个函数在多次调用中都接收了相同类型的参数,编译器可能会假设这个函数未来也只会接收这种类型的参数,并据此生成优化的机器码。

但是由于 JS 是一门动态的语言,有可能在执行过程中可能会有不同数据类型或者 Shape

可以简单理解为对引用类型的一种抽象,对于键名一样的引用类型有同样的 Shape 对 Shape 感兴趣的,可以 进一步看参考资料中的两个 YouTube 的视频

那么原先编译成的机器码就失效了,那么就要回退成更灵活的字节码,也就是deoptimize 去优化的过程。

前端常见 JS 引擎介绍

主流的JS引擎都有 ByteCode2MachineCode 和MachineCode2ByteCode 的环节,都有某种解析器(parser)、解释器(interpreter、) 编译器管道(compiler pipeline) 但具体处理细节是有差异的,可以简单了解一下。

Pasted image 20250716153708.png

V8

Pasted image 20250716153728.png V8 的解释器叫 Ignition,JIT 编译器叫 TurboFan

SpiderMonkey

Pasted image 20250716153842.png SpiderMonkey 的做法有所不同,有两个优化编译器

JavaScriptCore

Pasted image 20250716154206.png JSC 使用三个优化编译器,它从 LLint (low-level interpreter)生成字节码的低级解释器开始

参考资源

本篇的图片和思路来源于以下,侵权删

阿里工程师带你从零开始吃透编译技术:编译技术概述_哔哩哔哩_bilibili

JavaScript Engines: The Good Parts™ - Mathias Bynens & Benedikt Meurer - JSConf EU 2018

Understanding the V8 JavaScript Engine