AST(Abstract Syntax Tree)是源代碼的抽象語法結構樹狀表現形式,Webpack、ESLint、JSX、TypeScript 的編譯和模塊化規則之間的轉化都是經過 AST 來實現對代碼的檢查、分析以及編譯等操做。html
JavaScript 中想要使用 AST 進行開發,要知道抽象成語法樹以後的結構是什麼,裏面的字段名稱都表明什麼含義以及遍歷的規則,能夠經過 http://esprima.org/demo/parse... 來實現 JavaScript 語法的在線轉換。node
經過在線編譯工具,能夠將 function fn(a, b) {}
編譯爲下面的結構。git
{ "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 語法編譯成抽象語法樹後,須要對它進行遍歷、修該並從新編譯,遍歷樹結構的過程爲 「先序深度優先」。github
esprima
、estraverse
和 escodegen
模塊是操做 AST 的三個重要模塊,也是實現 babel
的核心依賴,下面是分別介紹三個模塊的做用。express
esprima 模塊的用法以下:npm
// 文件: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
方法轉換一個模塊。json
查看遍歷過程:數組
// 文件: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
屬性的對象被叫作一個節點,修改是獲取對應的類型並修改該節點中的屬性便可。babel
其實深度遍歷 AST 就是在遍歷每一層的 type
屬性,因此遍歷會分爲兩個階段,進入階段和離開階段,在 estraverse
的 traverse
方法中分別用參數指定的 entry
和 leave
兩個函數監聽,可是咱們通常只使用 entry
。async
下面的案例是一個段 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-core
和 babel-types
兩個模塊,其實這兩個模塊就是依賴 esprima
、estraverse
和 escodegen
的。
使用這兩個模塊須要安裝,命令以下:
npm install babel-core babel-types
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。
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 語法的過程,但願能夠爲後面本身實現一些編譯插件提供了思路。