编译器和解析器:V8如何执行一段JavaScript代码的|浏览器篇
# 编译器和解析器:V8如何执行一段JavaScript代码的
30 秒速记
- 理解
V8的执行机制,核心是弄清源码如何被转换为引擎可以处理并最终执行的形式。 - 需要建立一条完整概念链:编译器、解释器、
AST、字节码和JIT,而不是孤立记忆术语。 - 这套底层机制能够解释
Babel的语法转换、ESLint的静态检查,以及部分前端框架实现所依赖的代码分析思路。 - 前端工具迭代速度快,但源码解析与执行模型相对基础;掌握这些机制有助于判断工具能力及其限制。
理解 V8 执行代码,本质上要弄清源码怎样经过解析、转换,最后被真正执行。 核心链路可以抓住 AST、字节码和 JIT:源码先变成结构化表示,再由解释器执行,热点路径还可能被编译成机器码。掌握这条链路,也更容易理解 Babel 的语法转换、ESLint 的静态检查,以及部分框架的代码分析思路。需要注意,这些工具会使用自己的解析结果,概念相近,但节点结构和用途不等同于 V8 内部表示。
前面我们已经花了很多篇幅来介绍 JavaScript 是如何工作的,了解这些内容能帮助你从底层理解 JavaScript 的工作机制,从而能帮助你更好地理解和应用 JavaScript。
今天这篇文章我们就继续“向下”分析,站在 JavaScript 引擎 V8 的视角,来分析 JavaScript 代码是如何被执行的。
前端工具和框架的自身更新速度非常块,而且还不断有新的出现。要想追赶上前端工具和框架的更新速度,你就需要抓住那些本质的知识,然后才能更加轻松地理解这些上层应用。比如我们接下来要介绍的 V8 执行机制,能帮助你从底层了解 JavaScript,也能帮助你深入理解语言转换器 Babel、语法检查工具 ESLint、前端框架 Vue 和 React 的一些底层实现机制。因此,了解 V8 的编译流程能让你对语言以及相关工具有更加充分的认识。
要深入理解 V8 的工作原理,你需要搞清楚一些概念和原理,比如接下来我们要详细讲解的编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、字节码(Bytecode)、即时编译器(JIT)等概念,都是你需要重点关注的。
原理拆解: 源码进入 V8 后,首先经过词法和语法分析,形成表达程序结构的 AST;声明处理还会参与建立执行上下文。随后引擎把可执行结构转换成字节码,由解释器运行。运行过程中收集到的类型与调用信息,可以支持优化编译器为热点路径生成更高效的机器码;假设失效时,引擎可能退回较通用的执行形式。
最小验证: 输入两段源码:第一段只有函数声明,第二段包含非法语法。要验证的结论是“编译成功”和“函数真正执行”是两个不同阶段。以下代码可直接由现代 Node.js 执行:
const vm = require('node:vm');
const validSource = `
function add(a, b) {
console.log('function body executed');
return a + b;
}
add;
`;
const script = new vm.Script(validSource);
console.log('compiled');
const add = script.runInNewContext({ console });
console.log('loaded:', typeof add);
console.log('result:', add(2, 3));
try {
new vm.Script('function broken( {');
} catch (error) {
console.log('syntax:', error.name);
}
new vm.Script 会先解析并编译源码,但不会调用 add;runInNewContext 执行顶层代码并返回函数,最后一次调用才打印函数体日志。预期依次看到 compiled、loaded: function、函数体日志、result: 5 和 syntax: SyntaxError。
边界与排查: 这个实验只能观察阶段差异,不能直接取得 V8 内部的 AST、字节码或优化机器码。一次运行耗时也不能证明发生了 JIT 优化,因为预热、垃圾回收和系统调度都会干扰结果。分析 Babel、ESLint 时还应区分它们自己的解析器产物与 V8 内部表示:概念链路相似,节点结构和用途并不相同。
面试官追问
追问 1架构评审中有人说 V8 收到 JavaScript 后会一次性生成机器码并执行;面对一个只调用一次的函数,你会如何纠正这条执行链?
这条描述遗漏了 AST、字节码、解释器和热点优化。源码先经词法与语法分析形成 AST,再生成字节码供解释器执行;只有运行中识别出的热点路径才可能获得优化机器码,一次调用未必进入该路径。
追问 2团队要开发一个把新语法转换为旧语法的构建插件,负责人准备直接对源码做正则替换;你会要求先落地哪条处理链?
应先用适配目标语法的解析器把源码转换成 AST,再转换相关节点并由结果树生成源码。正则无法稳定表达嵌套语法和上下文关系,解析器版本或语法覆盖不足时仍会失败,不能把概念上的 AST 等同于 V8 内部表示。
追问 3安全团队要禁止某类函数调用,但代码里同时存在注释、字符串和多种等价写法;在字符串扫描与 AST 规则之间你如何选?
可靠检查应基于工具自身解析器生成的 AST,按调用结构匹配目标节点,从而避开注释和字符串造成的误报。还要明确解析器支持的语法及节点模型;它与 V8 的内部结构只是概念链路相似,不能直接混用。
追问 4发布流水线用 new vm.Script 检查源码后显示成功,但函数体里的业务日志始终没有出现;同事怀疑编译失效,你会怎样定位?
解析编译成功不代表函数已经被调用,new vm.Script 可先完成源码检查而不执行函数体。应区分创建脚本、执行顶层代码与实际调用函数三个阶段,并用非法语法验证 SyntaxError;这个实验不能直接证明内部字节码或 JIT 状态。
追问 5性能同学只跑一次基准就宣称某函数已被 TurboFan 优化,因为第二段日志更快;你会要求补充什么证据?
一次耗时变化不足以证明发生了 JIT 优化,预热、垃圾回收和系统调度都可能干扰结果。应先确认执行阶段与调用次数,再借助适当的引擎诊断能力观察优化状态;仅靠普通日志无法取得内部 AST、字节码或机器码结论。
# 编译器和解释器
30 秒速记
- 机器无法直接执行高级语言源码,因此运行前必须把源码转换成机器可处理的表示。
- 典型编译流程包括词法分析、语法分析、生成
AST、代码优化和生成机器码;成功后可保留独立的二进制文件供后续运行。 - 典型解释流程同样会完成词法与语法分析,但可从
AST继续生成字节码,再由解释器在运行时执行。 - 编译型与解释型的主要差别在执行时机和产物:前者承担预编译成本并复用二进制,后者在运行阶段持续参与转换与执行。
- 这是一种便于理解的典型分类;原文把
C/C++、Go归为编译型,把Python、JavaScript归为解释型,但实际语言实现可能混合解释与编译机制。
编译器和解释器都是把高级语言转换成机器可处理形式,差别主要在转换时机和最终产物。 典型编译流程会经过词法、语法分析生成 AST,再优化并生成可复用的机器码文件;编译失败时,可执行文件也无法正常生成。典型解释流程也会生成 AST,但通常继续生成字节码,由解释器在运行期间执行。把 C/C++、Go 视为编译型,把 Python、JavaScript 视为解释型,适合帮助入门理解,但实际引擎可能混合解释和编译机制。
之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。
编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
那编译器和解释器是如何“翻译”代码的呢?具体流程你可以参考下图

从图中你可以看出这二者的执行流程,大致可阐述为如下:
- 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功
- 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
面试官追问
追问 1评审会上有人断言 JavaScript 属于解释型语言,因此运行链路里绝不可能出现编译器;结合现代 V8,你会怎么回应?
这种二分法只能描述典型执行流程,不能替代具体引擎架构。V8 既用解释器执行字节码,也会用编译器处理热点代码,因此不能由“解释型”标签推出完全没有编译阶段;结论应限定到所讨论的实现。
追问 2一个固定源码的命令行程序每天启动很多次,团队在每次动态解释与预先生成可复用二进制之间选型;只按两类流程模型,你倾向哪边?
若运行环境允许且源码长期不变,预编译并复用二进制可避免每次重新翻译,更符合高频启动场景。代价是增加编译和产物管理流程,还可能受语言实现与目标平台限制,不能仅凭启动次数决定最终方案。
追问 3构建服务要求发布前拦截语法错误,但负责人认为解释型脚本只有执行到故障分支才会报错;你会如何设计验证?
应在发布阶段主动触发解析,让源码先经过词法和语法分析并生成 AST,非法语法即可在运行前暴露。解释流程也不意味着所有错误都延迟到业务分支执行时出现,具体报错时点取决于实现是否预解析。
追问 4同一份源码在编译型项目中没有生成安装包,而脚本环境启动时才抛出 SyntaxError;排查时怎样区分两条失败链?
编译型流程在生成机器码和可执行文件前完成词法、语法分析,失败会阻断二进制产物。解释型流程通常从源码生成 AST 和字节码后执行,错误可能在启动或解析相关代码时出现;仍需查看具体实现,不能把时点绝对化。
追问 5嵌入式设备内存有限,团队争论直接保留机器码还是保存字节码并由解释器执行;你会说明哪些取舍?
机器码可直接供处理器执行并复用,但编译产物和平台约束需要额外管理。字节码是介于 AST 与机器码之间的表示,可由解释器执行,并为后续热点编译提供基础;启动、内存和执行效率的权重仍应结合具体引擎测量。
