手摸手教你用 js 寫一個 js 解釋器
用 js 來 編譯 js 看起來是個高大上的東西,實(shí)際原理其實(shí)很簡單,無非就是利用 js 對象屬性可以用字符串表示 這個特性來實(shí)現(xiàn)的黑魔法罷了。
之所以看起來那么 深奧, 大概是由于網(wǎng)上現(xiàn)有的教程,都是動不動就先來個 babylon / @babel/parser 先讓大家看個一大串的 AST, 然后再貼出一大串的代碼,
直接遞歸 AST 處理所有類型的節(jié)點(diǎn). 最后成功的把我這樣的新手就被嚇跑了。
那么今天我寫這篇的目的,就是給大家一個淺顯易懂,連剛學(xué) js 的人都能看懂的 js2js 教程。
先來看一下效果

一個最簡單的解釋器
上面有提到,js 有個特性是 對象屬性可以用字符串表示,如 console.log 等價于 console['log'], 辣么根據(jù)這個特性,我們可以寫出一個兼容性極差,極其簡陋的雛形
function callFunction(fun, arg) {
this[fun](arg);
}
callFunction('alert', 'hello world');
// 如果你是在瀏覽器環(huán)境的話,應(yīng)該會彈出一個彈窗
既然是簡易版的,肯定是問題一大堆,js 里面得語法不僅僅是函數(shù)調(diào)用,我們看看賦值是如何用黑魔法實(shí)現(xiàn)的
function declareVarible(key, value) {
this[key] = value;
}
declareVarible.call(window, 'foo', 'bar');
// window.foo = 'bar'
Tips: const 可以利用 Object.defineProperty 實(shí)現(xiàn);
如果上面的代碼能看懂,說明你已經(jīng)懂得了 js 解釋器 的基本原理了,看不懂那只好怪我咯。
稍微加強(qiáng)一下
可以看出,上面為了方便, 我們把函數(shù)調(diào)用寫成了 callFunction('alert', 'hello world'); 但是著看起來一點(diǎn)都不像是 js 解釋器,
我們心里想要的解釋器至少應(yīng)該是長這樣的 parse('alert("hello world")''), 那么我們來稍微改造一下, 在這里我們要引入 babel 了,
不過先不用擔(dān)心, 我們解析出來的語法樹(AST)也是很簡單的。
import babelParser from '@babel/parser';
const code = 'alert("hello world!")';
const ast = babelParser.parse(code);
以上代碼, 解析出如下內(nèi)容
{
"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"
}
上面的內(nèi)容看起來很多,但是我們實(shí)際有用到到其實(shí)只是很小的一部分, 來稍微簡化一下, 把暫時用不到的字段先去掉
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "alert"
},
"arguments": [
{
"type": "Literal",
"value": "hello world!",
}
]
}
}
],
}
我們先大概瀏覽一遍 AST 里面的所有屬性名為 type 的數(shù)據(jù)
- ExpressionStatement
- CallExpression
- Identifier
- Literal
一共有 4 種類型, 那么接下來我們把這 4 種節(jié)點(diǎn)分別解析, 從最簡單的開始
Literal
{
"type": "Literal",
"value": "hello world!",
}
針對 Literal 的內(nèi)容, 我們需要的只有一個 value 屬性, 直接返回即可.
if(node.type === 'Literal') {
return node.value;
}
是不是很簡單?
Identifier
{
"type": "Identifier",
"name": "alert"
},
Identifier 同樣也很簡單, 它代表的就是我們已經(jīng)存在的一個變量, 變量名是node.name, 既然是已經(jīng)存在的變量, 那么它的值是什么呢?
if(node.type === 'Identifier') {
return {
name: node.name,
value:this[node.name]
};
}
上面的 alert 我們從 node.name 里面拿到的是一個字符, 通過 this['xxxxx'] 可以訪問到當(dāng)前作用域(這里是 window)里面的這個標(biāo)識符(Identifier)
ExpressionStatement
{
"type": "ExpressionStatement",
"expression": {...}
}
這個其實(shí)也是超簡單, 沒有什么實(shí)質(zhì)性的內(nèi)容, 真正的內(nèi)容都在 expression 屬性里,所以可以直接返回 expression 的內(nèi)容
if(node.type === 'ExpressionStatement') {
return parseAstNode(node.expression);
}
CallExpression
CallExpression 按字面的意思理解就是 函數(shù)調(diào)用表達(dá)式,這個稍微麻煩一點(diǎn)點(diǎn)
{
"type": "CallExpression",
"callee": {...},
"arguments": [...]
}
CallExpression 里面的有 2 個我們需要的字段:
callee 是 函數(shù)的引用, 里面的內(nèi)容是一個 Identifier, 可以用上面的方法處理.
arguments 里面的內(nèi)容是調(diào)用時傳的參數(shù)數(shù)組, 我們目前需要處理的是一個 Literal, 同樣上面已經(jīng)有處理方法了.
說到這里,相信你已經(jīng)知道怎么做了
if(node.type === 'CallExpression') {
// 函數(shù)
const callee = 調(diào)用 Identifier 處理器
// 參數(shù)
const args = node.arguments.map(arg => {
return 調(diào)用 Literal 處理器
});
callee(...args);
}
代碼
這里有一份簡單的實(shí)現(xiàn), 可以跑通上面的流程, 但也僅僅可以跑通上面而已, 其他的特性都還沒實(shí)現(xiàn)。
https://github.com/noahlam/practice-truth/tree/master/js2js
其他實(shí)現(xiàn)方式
除了上面我介紹得這種最繁瑣得方式外,其實(shí) js 還有好幾種可以直接執(zhí)行字符串代碼得方式
- 插入 script DOM
const script = document.createElement("script");
script.innerText = 'alert("hello world!")';
document.body.appendChild(script);
- eval
eval('alert("hello world!")')
- new Function
new Function('alert("hello world")')();
- setTimeout 家族
setTimeout('console.log("hello world")');
不過這些在小程序里面都被無情得封殺了...