1. 本文目标
来到最后一篇,我们把前面逐步实现的零件重新组装成一台完整的最小 JSVM。
这台教学版最小 JSVM 至少支持:
- 数字常量
- 字符串常量
- 加减乘除
- 变量声明
- 变量读取
- 表达式执行
- 简单函数调用
return- 简单
if
示例:
<span>function</span> <span>add</span>(<span>a, b</span>) {
<span>return</span> a + b;
}
<span>let</span> x = <span>add</span>(<span>1</span>, <span>2</span>);
x;
输出:
<span>3</span>
2. 最小 JSVM 总览
flowchart LR
A["Source"] --> B["Parser/AST"]
B --> C["Compiler AST->IR"]
C --> D["Emitter IR->Bytecode"]
D --> E["Runtime"]
E --> F["Result"]
形象化比喻:一条小型生产线
- Source:客户订单;
- AST:订单结构表;
- IR:生产步骤;
- Bytecode:机器按钮编号;
- Runtime:机器本体;
- Result:最终产品。
3. 支持语法范围
把范围收窄后,教学版最小实现支持:
| 能力 | 示例 |
|---|---|
| 数字 | `1` |
| 字符串 | `'a'` |
| 四则运算 | `1 + 2 * 3` |
| 变量声明 | `let a = 1` |
| 变量读取 | `a` |
| 函数声明 | `function add(a,b){...}` |
| 函数调用 | `add(1,2)` |
| return | `return a + b` |
| if | `if (x) { x = 2 }` |
不支持或作为扩展:
- 完整 JS parser;
- 完整块级作用域;
- 闭包完整语义;
- 对象和 class 完整语义;
- async/generator;
- module bundling。
4. 核心代码骨架
<span>const</span> <span>OPCODES</span> = {
<span>LOAD_CONST</span>: <span>1</span>,
<span>LOAD_SLOT</span>: <span>2</span>,
<span>INIT_SLOT</span>: <span>3</span>,
<span>STORE_SLOT</span>: <span>4</span>,
<span>BINARY</span>: <span>5</span>,
<span>JUMP</span>: <span>6</span>,
<span>JUMP_IF_FALSE</span>: <span>7</span>,
<span>MAKE_FUNCTION</span>: <span>8</span>,
<span>CALL</span>: <span>9</span>,
<span>RETURN</span>: <span>10</span>,
};
<span>const</span> <span>BINARY_OPS</span> = {
<span>'+'</span>: <span>1</span>,
<span>'-'</span>: <span>2</span>,
<span>'*'</span>: <span>3</span>,
<span>'/'</span>: <span>4</span>,
};
5. Runtime 最小实现
<span>function</span> <span>binary</span>(<span>op, left, right</span>) {
<span>switch</span> (op) {
<span>case</span> <span>BINARY_OPS</span>[<span>'+'</span>]: <span>return</span> left + right;
<span>case</span> <span>BINARY_OPS</span>[<span>'-'</span>]: <span>return</span> left - right;
<span>case</span> <span>BINARY_OPS</span>[<span>'*'</span>]: <span>return</span> left * right;
<span>case</span> <span>BINARY_OPS</span>[<span>'/'</span>]: <span>return</span> left / right;
<span>default</span>: <span>throw</span> <span>new</span> <span>Error</span>(<span>'bad op'</span>);
}
}
<span>function</span> <span>createEnv</span>(<span>slotNames, parent = <span>null</span></span>) {
<span>return</span> {
<span>values</span>: <span>new</span> <span>Array</span>(slotNames.<span>length</span>),
<span>states</span>: <span>new</span> <span>Array</span>(slotNames.<span>length</span>).<span>fill</span>(<span>false</span>),
slotNames,
parent,
};
}
<span>function</span> <span>readSlot</span>(<span>env, slot</span>) {
<span>if</span> (!env.<span>states</span>[slot]) <span>throw</span> <span>new</span> <span>ReferenceError</span>(<span>`<span>${env.slotNames[slot]}</span> not initialized`</span>);
<span>return</span> env.<span>values</span>[slot];
}
<span>function</span> <span>writeSlot</span>(<span>env, slot, value, init</span>) {
<span>if</span> (!init && !env.<span>states</span>[slot]) <span>throw</span> <span>new</span> <span>ReferenceError</span>(<span>`<span>${env.slotNames[slot]}</span> not initialized`</span>);
env.<span>values</span>[slot] = value;
env.<span>states</span>[slot] = <span>true</span>;
}
6. 执行函数
<span>function</span> <span>execute</span>(<span>metadata, functionId, parentEnv, args = []</span>) {
<span>const</span> fn = metadata.<span>functions</span>[functionId];
<span>const</span> env = <span>createEnv</span>(fn.<span>slotNames</span>, parentEnv);
<span>const</span> regs = <span>new</span> <span>Array</span>(fn.<span>registerCount</span>);
<span>const</span> code = metadata.<span>bytecode</span>;
<span>for</span> (<span>let</span> i = <span>0</span>; i < fn.<span>params</span>.<span>length</span>; i++) {
env.<span>values</span>[i] = args[i];
env.<span>states</span>[i] = <span>true</span>;
}
<span>let</span> pc = fn.<span>entry</span>;
<span>while</span> (pc < fn.<span>end</span>) {
<span>const</span> op = code[pc++];
<span>switch</span> (op) {
<span>case</span> <span>OPCODES</span>.<span>LOAD_CONST</span>:
regs[code[pc++]] = metadata.<span>constantPool</span>[code[pc++]];
<span>break</span>;
<span>case</span> <span>OPCODES</span>.<span>LOAD_SLOT</span>:
regs[code[pc++]] = <span>readSlot</span>(env, code[pc++]);
<span>break</span>;
<span>case</span> <span>OPCODES</span>.<span>INIT_SLOT</span>:
<span>writeSlot</span>(env, code[pc++], regs[code[pc++]], <span>true</span>);
<span>break</span>;
<span>case</span> <span>OPCODES</span>.<span>STORE_SLOT</span>:
<span>writeSlot</span>(env, code[pc++], regs[code[pc++]], <span>false</span>);
<span>break</span>;
<span>case</span> <span>OPCODES</span>.<span>BINARY</span>: {
<span>const</span> dst = code[pc++];
<span>const</span> left = code[pc++];
<span>const</span> right = code[pc++];
<span>const</span> bop = code[pc++];
regs[dst] = <span>binary</span>(bop, regs[left], regs[right]);
<span>break</span>;
}
<span>case</span> <span>OPCODES</span>.<span>JUMP</span>:
pc = code[pc];
<span>break</span>;
<span>case</span> <span>OPCODES</span>.<span>JUMP_IF_FALSE</span>: {
<span>const</span> condition = regs[code[pc++]];
<span>const</span> target = code[pc++];
<span>if</span> (!condition) pc = target;
<span>break</span>;
}
<span>case</span> <span>OPCODES</span>.<span>MAKE_FUNCTION</span>:
regs[code[pc++]] = { <span>functionId</span>: code[pc++], <span>parentEnv</span>: env };
<span>break</span>;
<span>case</span> <span>OPCODES</span>.<span>CALL</span>: {
<span>const</span> dst = code[pc++];
<span>const</span> fnValue = regs[code[pc++]];
<span>const</span> argc = code[pc++];
<span>const</span> argv = [];
<span>for</span> (<span>let</span> i = <span>0</span>; i < argc; i++) argv.<span>push</span>(regs[code[pc++]]);
regs[dst] = <span>execute</span>(metadata, fnValue.<span>functionId</span>, fnValue.<span>parentEnv</span>, argv);
<span>break</span>;
}
<span>case</span> <span>OPCODES</span>.<span>RETURN</span>:
<span>return</span> regs[code[pc++]];
<span>default</span>:
<span>throw</span> <span>new</span> <span>Error</span>(<span>`unknown opcode <span>${op}</span>`</span>);
}
}
}
7. 示例 Artifact
<span>const</span> metadata = {
<span>constantPool</span>: [<span>1</span>, <span>2</span>],
<span>functions</span>: [
{
<span>id</span>: <span>0</span>,
<span>name</span>: <span>null</span>,
<span>entry</span>: <span>0</span>,
<span>end</span>: <span>17</span>,
<span>registerCount</span>: <span>4</span>,
<span>slotNames</span>: [<span>'add'</span>, <span>'x'</span>],
<span>params</span>: [],
},
{
<span>id</span>: <span>1</span>,
<span>name</span>: <span>'add'</span>,
<span>entry</span>: <span>17</span>,
<span>end</span>: <span>26</span>,
<span>registerCount</span>: <span>3</span>,
<span>slotNames</span>: [<span>'a'</span>, <span>'b'</span>],
<span>params</span>: [<span>'a'</span>, <span>'b'</span>],
},
],
<span>bytecode</span>: [
<span>OPCODES</span>.<span>MAKE_FUNCTION</span>, <span>0</span>, <span>1</span>,
<span>OPCODES</span>.<span>INIT_SLOT</span>, <span>0</span>, <span>0</span>,
<span>OPCODES</span>.<span>LOAD_SLOT</span>, <span>1</span>, <span>0</span>,
<span>OPCODES</span>.<span>LOAD_CONST</span>, <span>2</span>, <span>0</span>,
<span>OPCODES</span>.<span>LOAD_CONST</span>, <span>3</span>, <span>1</span>,
<span>OPCODES</span>.<span>CALL</span>, <span>2</span>, <span>1</span>, <span>2</span>, <span>2</span>, <span>3</span>,
<span>OPCODES</span>.<span>INIT_SLOT</span>, <span>1</span>, <span>2</span>,
<span>OPCODES</span>.<span>LOAD_SLOT</span>, <span>3</span>, <span>1</span>,
<span>OPCODES</span>.<span>RETURN</span>, <span>3</span>,
<span>OPCODES</span>.<span>LOAD_SLOT</span>, <span>0</span>, <span>0</span>,
<span>OPCODES</span>.<span>LOAD_SLOT</span>, <span>1</span>, <span>1</span>,
<span>OPCODES</span>.<span>BINARY</span>, <span>2</span>, <span>0</span>, <span>1</span>, <span>BINARY_OPS</span>[<span>'+'</span>],
<span>OPCODES</span>.<span>RETURN</span>, <span>2</span>,
],
};
8. run(source) 的位置
这里还缺一个关键入口:parser。真正完整的 run(source) 需要先解析源码;教学版可以把 parse 简化为“输入已知示例时返回固定 AST”:
<span>function</span> <span>runKnownProgram</span>(<span></span>) {
<span>return</span> <span>execute</span>(metadata, <span>0</span>, <span>null</span>, []);
}
<span>console</span>.<span>log</span>(<span>runKnownProgram</span>()); <span>// 3</span>
因此,如果要扩展成真正的 run(source),就需要把 parser 接到现有编译与执行链路之前:
source -> parse -> AST -> compile -> IR -> emit -> metadata -> execute
9. 测试用例
教学版目标测试:
<span>run</span>(<span>'1 + 2'</span>) === <span>3</span>;
<span>run</span>(<span>'let a = 1; a;'</span>) === <span>1</span>;
<span>run</span>(<span>'let a = 1 + 2; a;'</span>) === <span>3</span>;
<span>run</span>(<span>'function add(a, b) { return a + b; } add(1, 2);'</span>) === <span>3</span>;
<span>run</span>(<span>'let x = 1; if (x) { x = 2; } x;'</span>) === <span>2</span>;
如果当前教学代码还没有完整 parser,这些测试可以先作为目标用例,随着 parser 能力补齐再逐步完成。
10. 源码设计复盘
| 教学版 | 正式源码 |
|---|---|
| 手写 / 简化 AST | Babel parser |
| 简化 FunctionBuilder | 完整 `FunctionBuilder` / `ScopeFrame` |
| 简化 IR | `src/compiler/ir.ts` |
| 简化 emit | `src/compiler/emit.ts` |
| 单一同步 runtime | `runtime-gen.ts` 中 sync/async/generator runtime |
| 简化 pack | `src/compiler/pack.ts` |
| 无 bundler | `src/compiler/bundler.ts` |
11. 安全与设计提醒
JSVM 并不是安全沙箱。即使源码被编成 bytecode,runtime 仍然在宿主 JS 环境中执行。如果暴露 globalObject、require、import 或原型对象,仍然可能产生任意代码执行、原型污染或沙箱逃逸风险。
因此,教学版不要声称具备安全隔离能力。正式工程若要用于不可信代码,需要把沙箱边界作为独立问题设计。
12. Mermaid 总复盘
flowchart TD
A["JavaScript Source"] --> B["Parser"]
B --> C["AST"]
C --> D["Lowering"]
D --> E["IR"]
E --> F["Emit"]
F --> G["Bytecode + ConstantPool"]
G --> H["Runtime Context"]
H --> I["Execute"]
I --> J["Result"]
13. 本文小结
至此,我们已经从 0 到 1 串起一台最小 JSVM 的完整链路:
- 最小栈式 VM;
- 数字 bytecode 与 constant pool;
- 寄存器式 VM;
- slot、environment 与 TDZ;
- AST 到 IR;
- IR 到 bytecode;
- 控制流;
- 函数调用;
- 闭包;
- 对象和属性访问;
- 打包输出;
- 完整最小复盘。
后续可以继续扩展:
- 完整 parser;
- 更完整的作用域链;
- 对象 / class 完整语义;
- async / generator;
- module bundling;
- 调试器;
- 混淆和保护策略。
适合想理解 JS 引擎编译执行链路的前端开发者按篇跟做,教学版代码骨架清晰,可直接作为自研脚本引擎或面试复习的起点。