用 JS 实现简易 JS 解释器:从原理到代码详解

作者:小括号

时间:2026-10-23

浏览:196

本文详解如何使用 JavaScript 编写一个简易的 JS 解释器。通过解析 Babel 生成的 AST 语法树,递归处理 Literal、Identifier、ExpressionStatement 和 CallExpression 四种节点类型,实现从代码字符串到执行结果的转换。文章提供核心代码逻辑与多种执行字符串代码的方式对比,帮助开发者深入理解前端底层执行机制。

文章目录

JavaScript 解释器基本原理

用 JavaScript 编写一个 JavaScript 解释器,听起来似乎是一项高深且复杂的技术,但实际上其核心原理非常直观。它主要利用了 JavaScript 中对象属性可以通过字符串动态访问这一特性,从而实现对代码执行的控制,这本质上是一种基于语言特性的巧妙应用。

之所以让许多开发者感到深奥,往往是因为现有的教程倾向于直接引入 babylon 或 @babel/parser 等工具,先展示庞大的抽象语法树(AST)结构,随后再贴出大量代码。

这种直接递归处理 AST 中所有类型节点的做法,对于新手来说门槛较高,容易让人望而却步。

因此,本文旨在提供一份浅显易懂的 JS2JS(JavaScript 到 JavaScript)教程,确保即使是刚入门的开发者也能理解其基本流程。

首先,让我们直观地看一下实现的效果。

JavaScript 解释器基本原理

简易解释器雏形实现

构建一个最基础的解释器雏形。

前文提到的核心特性是:对象属性名可以使用字符串表示,例如 console.log 等价于 console['log']。基于这一特性,我们可以构建一个兼容性极差但逻辑清晰的简易版本。


 function callFunction(fun, arg) {
 this[fun](arg);
 }
 callFunction('alert', 'hello world');
 // 如果你是在浏览器环境的话,应该会弹出一个弹窗

既然是一个简易版本,必然存在诸多局限。JavaScript 的语法不仅包含函数调用,还涉及赋值等操作。接下来,我们将探讨如何利用这一特性实现赋值逻辑。


 function declareVarible(key, value) {
 this[key] = value;
 }
 declareVarible.call(window, 'foo', 'bar');
 // window.foo = 'bar'

提示:如果需要模拟 const 的不可变性,可以利用 Object.defineProperty 来实现。

如果上述代码逻辑能够理解,说明你已经掌握了 JavaScript 解释器的基本工作原理;若仍有疑惑,可能需要重新审视基础概念。

引入 Babel 解析 AST

接下来,我们对解释器进行简单的功能增强。

在前面的示例中,为了方便演示,函数调用被简化为 callFunction('alert', 'hello world') 的形式。但这显然不符合标准 JavaScript 解释器的形态。我们期望的解释器应当能够解析类似 parse('alert("hello world")') 的代码。为此,我们需要引入 Babel 工具进行初步改造。

无需担心,我们解析生成的语法树(AST)结构其实非常直观。


import babelParser from '@babel/parser';
const code = 'alert("hello world!")';
const ast = babelParser.parse(code);

上述代码经过解析后,会生成如下结构的内容。


{
 "type": "Program",
 "start": 0,
 "end": 21,
 "body": [
 {
 "type": "ExpressionStatement",
 "start": 0,
 "end": 21,
 "expression": {
 "type": "CallExpression",
 "start": 0,
 "end": 21,
 "callee": {
 "type": "Identifier",
 "start": 0,
 "end": 5,
 "name": "alert"
 },
 "arguments": [
 {
 "type": "Literal",
 "start": 6,
 "end": 20,
 "value": "hello world!",
 "raw": "\"hello world!\""
 }
 ]
 }
 }
 ],
 "sourceType": "module"
}

虽然生成的内容看起来较为冗长,但实际执行中用到的字段仅占很小一部分。为了简化处理,我们可以暂时移除那些暂时不需要的字段。


{
 "type": "Program",
 "body": [
 {
 "type": "ExpressionStatement",
 "expression": {
 "type": "CallExpression",
 "callee": {
 "type": "Identifier",
 "name": "alert"
 },
 "arguments": [
 {
 "type": "Literal",
 "value": "hello world!",
 }
 ]
 }
 }
 ],
}

我们先大致浏览一下 AST 中所有 type 属性为以下类型的数据节点。

  • ExpressionStatement(表达式语句)
  • CallExpression(调用表达式)
  • Identifier(标识符)
  • Literal(字面量)

总共有这 4 种主要类型。接下来,我们将分别对这 4 种节点进行解析,并从最简单的类型开始。

节点解析:Literal

处理 Literal 节点。


{
 "type": "Literal",
 "value": "hello world!",
}

对于 Literal 类型的内容,我们只需要提取其 value 属性,并直接返回该值即可。


if(node.type === 'Literal') {
 return node.value;
}

这个处理过程是否非常简单?

节点解析:Identifier

处理 Identifier 节点。


{
 "type": "Identifier",
 "name": "alert"
},

Identifier 的处理同样简单,它代表一个已存在的变量,变量名存储在 node.name 中。既然这是一个已存在的变量,我们需要确定它的值是什么。


if(node.type === 'Identifier') {
 return {
 name: node.name,
 value:this[node.name]
 };
}

在前面的 alert 示例中,我们从 node.name 获取到的是一个字符串标识符。通过 this['标识符名'] 的方式,我们可以访问当前作用域(此处为 window 对象)中对应的标识符值。

节点解析:ExpressionStatement

处理 ExpressionStatement 节点。


{
 "type": "ExpressionStatement",
 "expression": {...}
}

这一类型的处理也非常直接,没有复杂的实质逻辑。真正的执行内容包含在 expression 属性中,因此可以直接返回 expression 属性的解析结果。


if(node.type === 'ExpressionStatement') {
 return parseAstNode(node.expression);
}

节点解析:CallExpression

处理 CallExpression 节点。

CallExpression 字面意思为函数调用表达式,其处理逻辑相对复杂一些。


{
 "type": "CallExpression",
 "callee": {...},
 "arguments": [...]
}

CallExpression 中包含两个我们需要的关键字段。

callee 字段表示函数的引用,其内部内容是一个 Identifier 节点,可以使用前文介绍的 Identifier 处理方法进行解析。

arguments 字段表示调用时传递的参数数组。在当前简化版中,我们需要处理的参数类型主要是 Literal,这在前文已有相应的处理方法。

至此,相信你已经掌握了实现的基本思路。


if(node.type === 'CallExpression') {
 // 函数
 const callee = 调用 Identifier 处理器
 // 参数
 const args = node.arguments.map(arg => {
 return 调用 Literal 处理器
 });
 callee(...args);
}

完整代码与执行方式对比

参考代码实现。

这里提供一份简单的实现代码,可以跑通上述流程。但请注意,这仅仅是一个基础版本,其他语言特性尚未实现。

完整代码仓库地址:https://github.com/noahlam/practice-truth/tree/master/js2js

其他代码执行方式探讨。

除了上述介绍的这种较为繁琐的手写解析方式外,JavaScript 还有几种可以直接执行字符串代码的内置方式。

1. 动态插入 script DOM 元素。


 const script = document.createElement("script");
 script.innerText = 'alert("hello world!")';
 document.body.appendChild(script);

2. 使用 eval 函数。


 eval('alert("hello world!")')

3. 使用 new Function 构造函数。


 new Function('alert("hello world")')();

4. 使用 setTimeout 等定时器函数(传入字符串参数时)。


setTimeout('console.log("hello world")');

需要注意的是,上述这些方式在微信小程序等受限环境中均被严格禁止使用。

以上就是本文的全部内容,希望对你的学习有所帮助,也欢迎大家继续支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章