近日,一篇题为《从 0 实现一个 Tiny JavaScript VM:项目架构拆解》的技术文章在开发者社区引发热议。该文作者以极简主义思路,从零开始构建了一个微型 JavaScript 虚拟机(VM),并详细拆解了其核心架构与实现逻辑。对于希望深入理解 JS 引擎底层原理的前端开发者而言,这无疑是一次极具价值的“手把手”实战教学。

为什么需要“手写”一个 JS VM?

在现代 Web 开发中,JavaScript 引擎(如 V8、SpiderMonkey)早已成为黑盒般的存在。开发者每天书写代码,却很少思考这些代码如何被解析、编译、执行。作者在文章中坦言,自己长期对“解释器/虚拟机如何工作”抱有强烈好奇心,而市面上多数教程要么过于抽象、要么深度绑定工业级引擎的庞杂源码。因此,他决定自己动手,从零写一个“能跑起来”的极小 JS VM——不求功能完备,只为揭示核心脉络。

架构拆解:三大核心模块

Tiny JavaScript VM 的整体架构遵循经典的“解析 - 编译 - 执行”三阶段流水线,但作者刻意简化了每个模块,以突出本质。

1. 词法分析与语法解析(Parser)

VM 的第一步是将源代码字符串转化为抽象语法树(AST)。文章采用的是手写递归下降解析器,而非使用工具生成。作者定义了简单的 Token 类型(数字、标识符、运算符、分号等),并通过递归函数处理表达式、语句、函数声明等结构。由于目标语言是 JS 的极小子集(仅支持变量声明、赋值、算术运算、函数调用、条件分支与循环),解析器体积控制在 500 行以内。

值得关注的是,解析器在构建 AST 时直接采用扁平指令序列(Bytecode)而非树结构,这一步实际上将语法解析与字节码生成合并,减少了中间表示的转化开销。

2. 字节码设计与编译器(Compiler)

Tiny VM 的指令集非常精简,不过十余条指令,例如:LOAD_CONST(加载常量)、LOAD_VAR(加载变量)、STORE_VAR(存储变量)、ADDSUBJUMPJUMP_IF_FALSECALLRET 等。每条指令占用固定大小(例如 4 字节),包含操作码和操作数。

编译器遍历 AST,将其线性化为指令序列。例如,x = 1 + 2 会被编译为:

LOAD_CONST 1
LOAD_CONST 2
ADD
STORE_VAR "x"

作者特意强调,这个编译器并未做任何优化(如常量折叠),完全忠实于原始表达式的计算顺序,目的是让读者清楚看到高级语言到低级指令的一一映射。

3. 虚拟机执行引擎(VM)

执行引擎的核心是一个基于栈的虚拟CPU:它拥有一个操作数栈(用于保存中间结果)、一个调用栈(用于函数调用)、一个指令指针(IP)以及一个变量存储区(类似“环境记录”)。执行时,VM 循环读取指令,根据操作码进行栈操作、变量读写、跳转或函数调用。

文章使用了 Python 实现整个 VM(约 300 行),因为 Python 本身具有动态类型与垃圾回收,避免了手动管理内存的干扰,使得逻辑更聚焦。但作者也指出,若用 C/Rust 实现,可以更贴近真实引擎的内存布局。

挑战与启发

虽然 Tiny JS VM 只能运行非常简单的脚本,但作者在文中记录了几个关键挑战:

  • 函数调用的作用域链:如何实现闭包?作者选择了最朴素的方式——每个函数在定义时捕获当前的环境,调用时创建新的作用域并链接到外层。
  • 变量定义与赋值:严格区分 var 声明和普通赋值,防止对未定义变量赋值报错。
  • 控制流指令的绝对地址:跳转指令直接使用编译时计算出的字节码偏移量,简单但正确。

意义与后续

对于读者而言,这份“从零到一”的拆解不仅提供了完整的代码(已开源至 GitHub),更重要的是展示了一种系统级思维:如何将高级语言特性逐步降级为机器可理解的指令序列。作者表示,未来计划增加对象、数组等复杂类型,并尝试引入 JIT 编译的雏形。

一位资深前端工程师在评论中写道:“读完这篇,我才真正理解 AST、字节码、栈机这些概念是如何连在一起的。很多面试题问‘JS 引擎如何执行代码’,这就是最清晰的答案。”

或许,Tiny JavaScript VM 的意义不在于实用,而在于启蒙——它让每一个“调包侠”都有机会成为“建造者”。对于渴望深入技术底层的人来说,这扇门已经打开。