原文出自:https://www.pandashen.com
AST 抽象語法樹簡介
AST(Abstract Syntax Tree)是源代碼的抽象語法結構樹狀表現形式,Webpack、ESLint、JSX、TypeScript 的編譯和模塊化規則之間的轉化都是通過 AST 來實現對代碼的檢查、分析以及編譯等操作。
JavaScript 語法的 AST 語法樹
JavaScript 中想要使用 AST 進行開發,要知道抽象成語法樹之後的結構是什麼,裏面的字段名稱都代表什麼含義以及遍歷的規則,可以通過 http://esprima.org/demo/parse... 來實現 JavaScript 語法的在線轉換。
通過在線編譯工具,可以將 function fn(a, b) {}
編譯爲下面的結構。
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "fn"
},
"params": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"body": []
},
"generator": false,
"expression": false,
"async": false
}
],
"sourceType": "script"
}
將 JavaScript 語法編譯成抽象語法樹後,需要對它進行遍歷、修該並重新編譯,遍歷樹結構的過程爲 “先序深度優先”。
esprima、estraverse 和 escodegen
esprima
、estraverse
和 escodegen
模塊是操作 AST 的三個重要模塊,也是實現 babel
的核心依賴,下面是分別介紹三個模塊的作用。
1、esprima 將 JS 轉換成 AST
esprima 模塊的用法如下:
// 文件:esprima-test.js
const esprima = require("esprima");
let code = "function fn() {}";
// 生成語法樹
let tree = esprima.parseScript(code);
console.log(tree);
// Script {
// type: 'Program',
// body:
// [ FunctionDeclaration {
// type: 'FunctionDeclaration',
// id: [Identifier],
// params: [],
// body: [BlockStatement],
// generator: false,
// expression: false,
// async: false } ],
// sourceType: 'script' }
通過上面的案例可以看出,通過 esprima
模塊的 parseScript
方法將 JS 代碼塊轉換成語法樹,代碼塊需要轉換成字符串,也可以通過 parseModule
方法轉換一個模塊。
2、estraverse 遍歷和修改 AST
查看遍歷過程:
// 文件:estraverse-test.js
const esprima = require("esprima");
const estraverse = require("estraverse");
let code = "function fn() {}";
// 遍歷語法樹
estraverse.traverse(esprima.parseScript(code), {
enter(node) {
console.log("enter", node.type);
},
leave() {
console.log("leave", node.type);
}
});
// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// leave Program
上面代碼通過 estraverse
模塊的 traverse
方法將 esprima
模塊轉換的 AST 進行了遍歷,並打印了所有的 type
屬性並打印,每含有一個 type
屬性的對象被叫做一個節點,修改是獲取對應的類型並修改該節點中的屬性即可。
其實深度遍歷 AST 就是在遍歷每一層的 type
屬性,所以遍歷會分爲兩個階段,進入階段和離開階段,在 estraverse
的 traverse
方法中分別用參數指定的 entry
和 leave
兩個函數監聽,但是我們一般只使用 entry
。
3、escodegen 將 AST 轉換成 JS
下面的案例是一個段 JS 代碼塊被轉換成 AST,並將遍歷、修改後的 AST 重新轉換成 JS 的全過程。
// 文件:escodegen-test.js
const esprima = require("esprima");
const estraverse = require("estraverse");
const escodegen = require("escodegen");
let code = "function fn() {}";
// 生成語法樹
let tree = esprima.parseScript(code);
// 遍歷語法樹
estraverse.traverse(tree, {
enter(node) {
// 修改函數名
if (node.type === "FunctionDeclaration") {
node.id.name = "ast";
}
}
});
// 編譯語法樹
let result = escodegen.generate(tree);
console.log(result);
// function ast() {
// }
在遍歷 AST 的過程中 params
值爲數組,沒有 type
屬性。
實現 Babel 語法轉換插件
實現語法轉換插件需要藉助 babel-core
和 babel-types
兩個模塊,其實這兩個模塊就是依賴 esprima
、estraverse
和 escodegen
的。
使用這兩個模塊需要安裝,命令如下:
npm install babel-core babel-types
1、plugin-transform-arrow-functions
plugin-transform-arrow-functions
是 Babel 家族成員之一,用於將箭頭函數轉換 ES5 語法的函數表達式。
// 文件:plugin-transform-arrow-functions.js
const babel = require("babel-core");
const types = require("babel-types");
// 箭頭函數代碼塊
let sumCode = `
const sum = (a, b) => {
return a + b;
}`;
let minusCode = `const minus = (a, b) => a - b;`;
// 轉化 ES5 插件
let ArrowPlugin = {
// 訪問者(訪問者模式)
visitor: {
// path 是樹的路徑
ArrowFunctionExpression(path) {
// 獲取樹節點
let node = path.node;
// 獲取參數和函數體
let params = node.params;
let body = node.body;
// 判斷函數體是否是代碼塊,不是代碼塊則添加 return 和 {}
if (!types.isBlockStatement(body)) {
let returnStatement = types.returnStatement(body);
body = types.blockStatement([returnStatement]);
}
// 生成一個函數表達式樹結構
let func = types.functionExpression(null, params, body, false, false);
// 用新的樹結構替換掉舊的樹結構
types.replaceWith(func);
}
}
};
// 生成轉換後的代碼塊
let sumResult = babel.transform(sumCode, {
plugins: [ArrowPlugin]
});
let minusResult = babel.transform(minusCode, {
plugins: [ArrowPlugin]
});
console.log(sumResult.code);
console.log(minusResult.code);
// let sum = function (a, b) {
// return a + b;
// };
// let minus = function (a, b) {
// return a - b;
// };
我們主要使用 babel-core
的 transform
方法將 AST 轉化成代碼塊,第一個參數爲轉換前的代碼塊(字符串),第二個參數爲配置項,其中 plugins
值爲數組,存儲修改 babal-core
轉換的 AST 的插件(對象),使用 transform
方法將舊的 AST 處理成新的代碼塊後,返回值爲一個對象,對象的 code
屬性爲轉換後的代碼塊(字符串)。
內部修改通過 babel-types
模塊提供的方法實現,API 可以到 https://github.com/babel/babe... 中查看。
ArrowPlugin
就是傳入 transform
方法的插件,必須含有 visitor
屬性(固定),值同爲對象,用於存儲修改語法樹的方法,方法名要嚴格按照 API,對應的方法會修改 AST 對應的節點。
在 types.functionExpression
方法中參數分別代表,函數名(匿名函數爲 null
)、函數參數(必填)、函數體(必填)、是否爲 generator
函數(默認 false
)、是否爲 async
函數(默認 false
),返回值爲修改後的 AST,types.replaceWith
方法用於替換 AST,參數爲新的 AST。
2、plugin-transform-classes
plugin-transform-classes
也是 Babel 家族中的成員之一,用於將 ES6 的 class
類轉換成 ES5 的構造函數。
// 文件:plugin-transform-classes.js
const babel = require("babel-core");
const types = require("babel-types");
// 類
let code = `
class Person {
constructor(name) {
this.name = name;
}
getName () {
return this.name;
}
}`;
// 將類轉化 ES5 構造函數插件
let ClassPlugin = {
visitor: {
ClassDeclaration(path) {
let node = path.node;
let classList = node.body.body;
// 將取到的類名轉換成標識符 { type: 'Identifier', name: 'Person' }
let className = types.identifier(node.id.name);
let body = types.blockStatement([]);
let func = types.functionDeclaration(className, [], body, false, false);
path.replaceWith(func);
// 用於存儲多個原型方法
let es5Func = [];
// 獲取 class 中的代碼體
classList.forEach((item, index) => {
// 函數的代碼體
let body = classList[index].body;
// 獲取參數
let params = item.params.length ? item.params.map(val => val.name) : [];
// 轉化參數爲標識符
params = types.identifier(params);
// 判斷是否是 constructor,如果構造函數那就生成新的函數替換
if (item.kind === "constructor") {
// 生成一個構造函數樹結構
func = types.functionDeclaration(className, [params], body, false, false);
} else {
// 其他情況是原型方法
let proto = types.memberExpression(className, types.identifier("prototype"));
// 左側層層定義標識符 Person.prototype.getName
let left = types.memberExpression(proto, types.identifier(item.key.name));
// 右側定義匿名函數
let right = types.functionExpression(null, [params], body, false, false);
// 將左側和右側進行合併並存入數組
es5Func.push(types.assignmentExpression("=", left, right));
}
});
// 如果沒有原型方法,直接替換
if (es5Func.length === 0) {
path.replaceWith(func);
} else {
es5Func.push(func);
// 替換 n 個節點
path.replaceWithMultiple(es5Func);
}
}
}
};
// 生成轉換後的代碼塊
result = babel.transform(code, {
plugins: [ClassPlugin]
});
console.log(result.code);
// Person.prototype.getName = function () {
// return this.name;
// }
// function Person(name) {
// this.name = name;
// }
上面這個插件的實現要比 plugin-transform-arrow-functions
複雜一些,歸根結底還是將要互相轉換的 ES6 和 ES5 語法樹做對比,找到他們的不同,並使用 babel-types
提供的 API 對語法樹對應的節點屬性進行修改並替換語法樹,值得注意的是 path.replaceWithMultiple
與 path.replaceWith
不同,參數爲一個數組,數組支持多個語法樹結構,可根據具體修改語法樹的場景選擇使用,也可根據不同情況使用不同的替換方法。
總結
通過本節我們瞭解了什麼是 AST 抽象語法樹、抽象語法樹在 JavaScript 中的體現以及在 NodeJS 中用於生成、遍歷和修改 AST 抽象語法樹的核心依賴,並通過使用 babel-core
和 babel-types
兩個模塊簡易模擬了 ES6 新特性轉換爲 ES5 語法的過程,希望可以爲後面自己實現一些編譯插件提供了思路。