也就前兩天,面試大廠,其中有那麼一個問題:javascript
瞭解過抽象語法樹,又稱AST,有學習過,也寫過一個基於AST的乞丐版模板引擎,先是詞法解析token,而後生產抽象語法樹,而後更改抽象語法樹,固然這是插件作的事情,最後根據新的AST生成代碼。java
沒有,只是看過相關文檔node
應該能夠吧...git
遂卒....github
開玩笑的,既然提到了,又沒回答上來什麼,哎喲我這暴脾氣,一想到今晚就睡不着,連夜把它擼了。面試
那麼咱們來從零寫個插件吧。bash
寫一個預計算簡單表達式的插件babel
Before:學習
const result = 1 + 2 + 3 + 4 + 5;
複製代碼
After:ui
const result = 15;
複製代碼
以上的例子可能你們不會常常遇到,由於傻x纔會這麼寫,可是有可能你會這麼寫
setTimeout(function(){
// do something
}, 1000 * 2) // 插件要作的事,就是把 1000 * 2 替換成 2000
複製代碼
再寫代碼以前,你須要明白Babel它的原理,簡單點說: Babel解析成AST,而後插件更改AST,最後由Babel輸出代碼
那麼Babel的插件模塊須要你暴露一個function,function內返回visitor
module.export = function(babel){
return {
visitor:{
}
}
}
複製代碼
visitor是對各種型的AST節點作處理的地方,那麼咱們怎麼知道Babel生成了的AST有哪些節點呢?
很簡單,你能夠把Babel轉換的結果打印出來,或者這裏有傳送門: AST explorer
這裏咱們看到 const result = 1 + 2
中的1 + 1
是一個BinaryExpression
節點,那麼在visitor中,咱們就處理這個節點
var babel = require('babel-core');
var t = require('babel-types');
const visitor = {
BinaryExpression(path) {
const node = path.node;
let result;
// 判斷表達式兩邊,是否都是數字
if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
// 根據不一樣的操做符做運算
switch (node.operator) {
case "+":
result = node.left.value + node.right.value;
break
case "-":
result = node.left.value - node.right.value;
break;
case "*":
result = node.left.value * node.right.value;
break;
case "/":
result = node.left.value / node.right.value;
break;
case "**":
let i = node.right.value;
while (--i) {
result = result || node.left.value;
result = result * node.left.value;
}
break;
default:
}
}
// 若是上面的運算有結果的話
if (result !== undefined) {
// 把表達式節點替換成number字面量
path.replaceWith(t.numericLiteral(result));
}
}
};
module.exports = function (babel) {
return {
visitor
};
}
複製代碼
插件寫好了,咱們運行下插件試試
const babel = require("babel-core");
const result = babel.transform("const result = 1 + 2;",{
plugins:[
require("./index")
]
});
console.log(result.code); // const result = 3;
複製代碼
與預期一致,那麼轉換 const result = 1 + 2 + 3 + 4 + 5;
呢?
結果是: const result = 3 + 3 + 4 + 5;
這就奇怪了,爲何只計算了1 + 2
以後,就沒有繼續往下運算了?
咱們看一下這個表達式的AST樹
你會發現Babel解析成表達式裏面再嵌套表達式。
表達式( 表達式( 表達式( 表達式(1 + 2) + 3) + 4) + 5)
複製代碼
而咱們的判斷條件並不符合全部的,只符合1 + 2
// 判斷表達式兩邊,是否都是數字
if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {}
複製代碼
那麼咱們得改一改
第一次計算1 + 2
以後,咱們會獲得這樣的表達式
表達式( 表達式( 表達式(3 + 3) + 4) + 5)
複製代碼
其中 3 + 3
又符合了咱們的條件, 咱們經過向上遞歸的方式遍歷父級節點
又轉換成這樣:
表達式( 表達式(6 + 4) + 5)
表達式(10 + 5)
15
複製代碼
// 若是上面的運算有結果的話
if (result !== undefined) {
// 把表達式節點替換成number字面量
path.replaceWith(t.numericLiteral(result));
let parentPath = path.parentPath;
// 向上遍歷父級節點
parentPath && visitor.BinaryExpression.call(this, parentPath);
}
複製代碼
到這裏,咱們就得出告終果 const result = 15;
那麼其餘運算呢:
const result = 100 + 10 - 50
>>> const result = 60;
const result = (100 / 2) + 50
>>> const result = 100;
const result = (((100 / 2) + 50 * 2) / 50) ** 2
>>> const result = 9;
到這裏,已經向你大概的講解了,如何編寫一個Babel插件,不再怕面試官問我答不出什麼了哈...
你覺得這就完了嗎?
並無
若是轉換這樣呢: const result = 0.1 + 0.2;
預期確定是0.3
, 可是實際上,Javascript有浮點計算偏差,得出的結果是0.30000000000000004
那是否是這個插件就沒卵用?
這就須要你去矯正浮點運算偏差了,可使用Big.js;
好比: result = node.left.value + node.right.value;
改爲 result = +new Big(node.left.value).plus(node.right.value);
你覺得完了嗎? 這個插件還能夠作不少
好比: Math.PI * 2
>>> 6.283185307179586
好比: Math.pow(2, 2)
>>> 4
...
...
最後上項目地址: github.com/axetroy/bab…