用 JS 实现简易 JS 解释器:从原理到代码详解
本文详解如何使用 JavaScript 编写一个简易的 JS 解释器。通过解析 Babel 生成的 AST 语法树,递归处理 Literal、Identifier、ExpressionStatement 和 CallExpression 四种节点类型,实现从代码字符串到执行结果的转换。文章提供核心代码逻辑与多种执行字符串代码的方式对比,帮助开发者深入理解前端底层执行机制。
JavaScript 解释器基本原理
用 JavaScript 编写一个 JavaScript 解释器,听起来似乎是一项高深且复杂的技术,但实际上其核心原理非常直观。它主要利用了 JavaScript 中对象属性可以通过字符串动态访问这一特性,从而实现对代码执行的控制,这本质上是一种基于语言特性的巧妙应用。
之所以让许多开发者感到深奥,往往是因为现有的教程倾向于直接引入 babylon 或 @babel/parser 等工具,先展示庞大的抽象语法树(AST)结构,随后再贴出大量代码。
这种直接递归处理 AST 中所有类型节点的做法,对于新手来说门槛较高,容易让人望而却步。
因此,本文旨在提供一份浅显易懂的 JS2JS(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")');
需要注意的是,上述这些方式在微信小程序等受限环境中均被严格禁止使用。
以上就是本文的全部内容,希望对你的学习有所帮助,也欢迎大家继续支持本站。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
