人人都会AI编程

编译执行流程:解析 → AST → 字节码 → 机器码

更新时间:2026-07-11

现代 JavaScript 引擎(以 V8 为例)在执行代码时,并不是直接逐行解释源码,而是经过了一套精心设计的编译流水线。理解这条流水线,能帮助你明白为什么 JavaScript 既能快速启动,又能在长时间运行后达到接近编译型语言的性能。整个流程可以概括为四个核心阶段:解析、生成 AST、编译为字节码、编译为机器码。

解析(Parsing):从文本到结构化数据

当 V8 拿到一段 JavaScript 源码时,第一步是将这一大坨文本变成引擎能够理解的结构化表示。这个阶段分为两个步骤:

  1. 词法分析(Lexical Analysis / Tokenization)

扫描源码字符串,将其拆分为一个个有意义的词法单元(token),比如关键字 function、标识符 add、操作符 +、数字字面量 1。同时,分词器会忽略空格、注释等对执行无影响的字符。

  1. 语法分析(Syntax Analysis / Parsing)

将 token 流按照 JavaScript 文法规则构建成一颗抽象语法树(Abstract Syntax Tree,简称 AST)。AST 以树状结构表达了代码的逻辑关系,每个节点代表一个语法结构,比如一个函数声明、一个变量声明、一个条件分支等。

整个过程 V8 采用 惰性解析(Lazy Parsing):首次解析时,对于没有立即被调用的函数体,只做浅层扫描,不生成完整的 AST 树,直到该函数真正需要被执行时再完全解析。这一策略极大地减少了初始启动时间,因为它避免了在页面加载时对所有 JS 代码做全量深度解析。

生成字节码(Bytecode Generation)

拿到 AST 之后,不能直接让 CPU 去执行 AST 节点。AST 仍然太抽象,与底层硬件的距离太远。V8 的下一步是将 AST 编译为字节码(Bytecode)

字节码是一种介于 AST 和机器码之间的中间表示,它是平台无关的指令序列。每一条字节码指令都很短,比如 LdaSmi [42] 表示“加载小整数 42 到累加器”,Add 表示“将累加器中的值与操作数栈顶的值相加”。字节码的设计目标是:

  • 比 AST 紧凑:字节码可以被高效保存、缓存和执行。
  • 比机器码抽象:不需要关心具体的 CPU 架构,方便提高引擎的可移植性。
  • 降低内存占用:相比直接生成全量机器码,字节码大大减少了内存消耗,这对移动设备尤其重要。

V8 中负责将 AST 转换为字节码的组件是 Ignition 解释器。Ignition 不仅生成字节码,也负责直接解释执行这些字节码。当函数首次被调用时,往往是通过 Ignition 解释执行字节码,而不是直接跑机器码,这是启动速度与内存优先生存的关键。

从字节码到机器码(Optimizing Compilation)

解释执行字节码足够快吗?对于大多数只执行一两次的代码,是的。但对于需要反复执行的热点代码(比如循环体内的函数、高频触发的事件回调),纯粹的解释执行还不够。此时,V8 会启动另一条管道:将热点函数的字节码进一步编译为高度优化的机器码(Machine Code)

这个阶段由 V8 的优化编译器 TurboFan 完成。TurboFan 会做大量的优化工作:

  • 内联展开:小函数体直接嵌入调用点,避免函数调用开销。
  • 逃逸分析:如果对象只在函数内部使用,可以在栈上分配而不需要堆分配。
  • 隐藏类(Hidden Class)推断:根据对象属性的结构推断出固定的内存布局,快速访问属性。
  • 类型特化:根据代码运行时的类型反馈(type feedback)推测变量的具体类型,如果后续调用时类型不变,就直接采用针对该类型的特化指令。

这种基于运行信息做激进优化、并在假设失效时回退的机制,叫做即时编译(Just-In-Time compilation,JIT)。一张典型的 V8 执行生命周期图:

  JavaScript 源码
       ↓  (Parser)
     AST
       ↓  (Ignition)
     字节码 ← 解释执行(启动快,内存省)
       ↓ (当代码变热)
   TurboFan 优化编译
       ↓
     机器码 ← 高速执行

如果优化后使用的类型假设被违反(比如原本推测参数总是整数,后面传入了一个字符串),TurboFan 会触发去优化(Deoptimization),放弃优化后的机器码,退回字节码解释执行,必要时再重新优化。这保证了性能在大多数时候很快,极端情况下也保持正确。

这一切对开发者的意义

了解这条编译流水线,能帮助你在日常开发中写出对引擎更友好的代码:

  • 避免不必要的解析冗余:打包工具通过代码分割、按需加载减少首屏解析量,这与 V8 的惰性解析相互配合。还有一点:尽量保持函数的作用域简洁,避免在顶层编写大量初始化代码(它们会被立即全量解析)。
  • 帮助 V8 做内联缓存:避免频繁改变对象的形状(属性的顺序、类型),尽量在构造函数中一次性初始化所有属性,或者使用 TypeScript 固定类型。这让 V8 的隐藏类推断更准确,减少去优化。
  • 不滥用 eval() 和 arguments:动态执行的代码会让引擎放弃大量优化机会,因为无法在编译时进行静态分析。

简单来说,你遵守了这些规则,就能让 JavaScript 代码跑得更快、更省内存,而不需要深入理解每一项优化细节。引擎很聪明,但它也需要你的配合。