用 js 來 編譯 js
看起來是個高大上的東西,實際原理其實很簡單,無非就是利用 js 對象屬性能夠用字符串表示
這個特性來實現的黑魔法罷了。 之因此看起來那麼 深奧
, 大概是因爲網上現有的教程,都是動不動就先來個 babylon / @babel/parser
先讓你們看個一大串的 AST
, 而後再貼出一大串的代碼, 直接遞歸 AST 處理全部類型的節點. 最後成功的把我這樣的新手就被嚇跑了。javascript
那麼今天我寫這篇的目的,就是給你們一個淺顯易懂,連剛學 js 的人都能看懂的 js2js 教程。java
先來看一下效果node
上面有提到,js 有個特性是 對象屬性能夠用字符串表示
,如 console.log 等價於 console['log'], 辣麼根據這個特性,咱們能夠寫出一個兼容性極差,極其簡陋的雛形git
function callFunction(fun, arg) {
this[fun](arg);
}
callFunction('alert', 'hello world');
// 若是你是在瀏覽器環境的話,應該會彈出一個彈窗
複製代碼
既然是簡易版的,確定是問題一大堆,js 裏面得語法不單單是函數調用,咱們看看賦值是如何用黑魔法實現的github
function declareVarible(key, value) {
this[key] = value;
}
declareVarible.call(window, 'foo', 'bar');
// window.foo = 'bar'
複製代碼
Tips: const 能夠利用 Object.defineProperty 實現;express
若是上面的代碼能看懂,說明你已經懂得了 js 解釋器
的基本原理了,看不懂那隻好怪我咯。小程序
能夠看出,上面爲了方便, 咱們把函數調用寫成了 callFunction('alert', 'hello world');
可是着看起來一點都不像是 js 解釋器
, 咱們內心想要的解釋器至少應該是長這樣的 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"
}
複製代碼
上面的內容看起來不少,可是咱們實際有用到到其實只是很小的一部分, 來稍微簡化一下, 把暫時用不到的字段先去掉babel
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "alert"
},
"arguments": [
{
"type": "Literal",
"value": "hello world!",
}
]
}
}
],
}
複製代碼
咱們先大概瀏覽一遍 AST 裏面的全部屬性名爲 type
的數據
一共有 4 種類型, 那麼接下來咱們把這 4 種節點分別解析, 從最簡單的開始
{
"type": "Literal",
"value": "hello world!",
}
複製代碼
針對 Literal 的內容, 咱們須要的只有一個 value 屬性, 直接返回便可.
if(node.type === 'Literal') {
return node.value;
}
複製代碼
是否是很簡單?
{
"type": "Identifier",
"name": "alert"
},
複製代碼
Identifier 一樣也很簡單, 它表明的就是咱們已經存在的一個變量, 變量名是node.name, 既然是已經存在的變量, 那麼它的值是什麼呢?
if(node.type === 'Identifier') {
return {
name: node.name,
value:this[node.name]
};
}
複製代碼
上面的 alert
咱們從 node.name
裏面拿到的是一個字符, 經過 this['xxxxx']
能夠訪問到當前做用域(這裏是 window)裏面的這個標識符(Identifier)
{
"type": "ExpressionStatement",
"expression": {...}
}
複製代碼
這個其實也是超簡單, 沒有什麼實質性的內容, 真正的內容都在 expression
屬性裏,因此能夠直接返回 expression 的內容
if(node.type === 'ExpressionStatement') {
return parseAstNode(node.expression);
}
複製代碼
CallExpression 按字面的意思理解就是 函數調用表達式,這個稍微麻煩一點點
{
"type": "CallExpression",
"callee": {...},
"arguments": [...]
}
複製代碼
CallExpression 裏面的有 2 個咱們須要的字段:
callee 是 函數的引用, 裏面的內容是一個 Identifier, 能夠用上面的方法處理.
arguments 裏面的內容是調用時傳的參數數組, 咱們目前須要處理的是一個 Literal, 一樣上面已經有處理方法了.
說到這裏,相信你已經知道怎麼作了
if(node.type === 'CallExpression') {
// 函數
const callee = 調用 Identifier 處理器
// 參數
const args = node.arguments.map(arg => {
return 調用 Literal 處理器
});
callee(...args);
}
複製代碼
這裏有一份簡單的實現, 能夠跑通上面的流程, 但也僅僅能夠跑通上面而已, 其餘的特性都還沒實現。
除了上面我介紹得這種最繁瑣得方式外,其實 js 還有好幾種能夠直接執行字符串代碼得方式
const script = document.createElement("script");
script.innerText = 'alert("hello world!")';
document.body.appendChild(script);
複製代碼
eval('alert("hello world!")')
複製代碼
new Function('alert("hello world")')();
複製代碼
setTimeout('console.log("hello world")');
複製代碼
不過這些在小程序裏面都被無情得封殺了...