Abstract Syntax Tree ,AST
)?百度百科是這麼解釋的:html
在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。前端
聽起來仍是很繞,不要緊,你能夠簡單理解爲 它就是你所寫代碼的的樹狀結構化表現形式。vue
有了這棵樹,咱們就能夠經過操縱這顆樹,精準的定位到聲明語句、賦值語句、運算語句等等,實現對代碼的分析、優化、變動等操做。node
AST在平常業務中也許很難涉及到,有可能你尚未聽過,但其實不少時候你已經在使用它了,只是沒有太多關注而已,如今流行的 webpack
,eslint
等不少插件或者包都有涉及~react
聊到AST
的用途,其應用很是普遍,下面我簡單羅列了一些:webpack
IDE
的錯誤提示、代碼格式化、代碼高亮、代碼自動補全等JSLint
、JSHint
對代碼錯誤或風格的檢查等webpack
、rollup
進行代碼打包等CoffeeScript
、TypeScript
、JSX
等轉化爲原生Javascript
其實它的用途,還不止這些,若是說你已經不知足於實現枯燥的業務功能,想寫出相似react
、vue
這樣的牛逼框架,或者想本身搞一套相似webpack
、rollup
這樣的前端自動化打包工具,那你就必須弄懂AST
。es6
在瞭解如何生成AST
以前,有必要了解一下Parser
(常見的Parser
有esprima
、traceur
、acorn
、shift
等)web
JS Parser
實際上是一個解析器,它是將js
源碼轉化爲抽象語法樹(AST
)的解析器。express
整個解析過程主要分爲如下兩個步驟:npm
什麼是語法單元?
語法單元是被解析語法當中具有實際意義的最小單元,簡單的來理解就是天然語言中的詞語。
舉個例子來講,下面這段話:
「2019年是祖國70週年」
咱們能夠把這句話拆分紅最小單元,即:2019年、是、祖國、70、週年。
這就是咱們所說的分詞,也是最小單元,由於若是咱們把它再拆分出去的話,那就沒有什麼實際意義了。
Javascript
代碼中的語法單元主要包括如下這麼幾種:
var
、let
、const
等if
、else
這些關鍵字,又或者是 true
、false
這些內置常量+
、-
、 *
、/
等若是咱們以最簡單的複製語句爲例的話,以下?
var a = 1;
複製代碼
經過分詞,咱們能夠獲得以下結果:
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": ";"
}
]
複製代碼
爲了方便,我直接在 esprima/parser 這個網站生成分詞的~
什麼是語法分析?
上面咱們已經獲得了咱們分詞的結果,須要將詞彙進行一個立體的組合,肯定詞語之間的關係,肯定詞語最終的表達含義。
簡單來講語法分析是對語句和表達式識別,肯定以前的關係,這是個遞歸過程。
上面咱們經過語法分析,能夠獲得以下結果:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
複製代碼
這就是 var a = 1
所轉換的 AST
;
這裏推薦一下astexplorer AST
的可視化工具,astexplorer,能夠直接進行對代碼進行AST
轉換~
上面畫了很大篇幅聊了聊AST
是什麼以及它是如何生成的,說到底,仍是不知道AST
這玩意有啥用,到底怎麼使用。。
ok~ 接下來咱們來一塊兒見證奇蹟。
我相信大部分同窗對 babel
這個庫不陌生,如今的作前端模塊化開發過程當中中必定少不了它,由於它能夠幫你將 ECMAScript 2015+
版本的代碼轉換爲向後兼容的 JavaScript
語法,以便可以運行在當前和舊版本的瀏覽器或其餘環境中,你不用爲新語法的兼容性考慮~
而實際上呢,babel
中的不少功能都是靠修改 AST
實現的。
首先,咱們來看一個簡單的例子,咱們如何將 es6
中的 箭頭函數
轉換成 es5
中的 普通函數,即:
const sum=(a,b)=>a+b;
複製代碼
咱們如何將上面簡單的 sum
箭頭函數轉換成下面的形式:
const sum = function(a,b){
return a+b;
}
複製代碼
想一想看,有什麼思路?
若是說你不瞭解 AST
的話,這無疑是一個很困難的問題,根本無從下手,若是你瞭解 AST
的話,這將是一個很是 easy
的例子。
接下來咱們看看如何實現?
須要操做 AST
代碼,這裏,咱們須要藉助兩個庫,分別是 @babel/core
和 babel-types
。
其中 @babel/core
是 babel
的核心庫,用來實現核心轉換引擎,babel-types
類型判斷,用於生成AST
零部件
cd
到一個你喜歡的目錄,經過 npm -y init
初始化操做後,經過 npm i @babel/core babel-types -D
安裝依賴
要進行轉換以前,咱們須要進行分析,首先咱們先經過 astexplorer 查看目標代碼跟咱們如今的代碼有什麼區別。
源代碼的 AST
結構以下:
// 源代碼的 AST
{
"type": "Program",
"start": 0,
"end": 21,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 21,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 20,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "sum"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 10,
"end": 20,
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 11,
"end": 12,
"name": "a"
},
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"start": 17,
"end": 20,
"left": {
"type": "Identifier",
"start": 17,
"end": 18,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 19,
"end": 20,
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
複製代碼
目標代碼的 AST
結構以下:
// 目標代碼的 `AST`
{
"type": "Program",
"start": 0,
"end": 48,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 48,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 47,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "sum"
},
"init": {
"type": "FunctionExpression",
"start": 12,
"end": 47,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 22,
"end": 23,
"name": "a"
},
{
"type": "Identifier",
"start": 25,
"end": 26,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 28,
"end": 47,
"body": [
{
"type": "ReturnStatement",
"start": 32,
"end": 45,
"argument": {
"type": "BinaryExpression",
"start": 39,
"end": 44,
"left": {
"type": "Identifier",
"start": 39,
"end": 40,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 43,
"end": 44,
"name": "b"
}
}
}
]
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
複製代碼
其中裏面的 start
和 end
咱們不用在乎,其只是爲了標記其所在代碼的位置。
咱們關心的是 body
裏面的內容,經過對比,咱們發現主要不一樣就在於 init
這一段,一個是 ArrowFunctionExpression
, 另外一個是 FunctionExpression
, 咱們只須要將 ArrowFunctionExpression
下的內容改形成跟 FunctionExpression
便可。
咱們建一個 arrow.js
的文件,引入上面的兩個庫,即
//babel 核心庫,用來實現核心轉換引擎
const babel = require('@babel/core')
//類型判斷,生成AST零部件
const types = require('babel-types')
//源代碼
const code = `const sum=(a,b)=>a+b;` //目標代碼 const sum = function(a,b){ return a + b }
複製代碼
這裏咱們須要用到 babel
中的 transform
方法,它能夠將 js
代碼轉換成 AST
,過程當中能夠經過使用 plugins
對 AST
進行改造,最終生成新的 AST
和 js
代碼,其整個過程用網上一個比較貼切的圖就是:
關於 babel transform
詳細用法,這裏很少作討論,感興趣的話能夠去官網瞭解~
其主要用法以下:
//transform方法轉換code
//babel先將代碼轉換成ast,而後進行遍歷,最後輸出code
let result = babel.transform(code,{
plugins:[
{
visitor
}
]
})
複製代碼
其核心在於插件 visitor
的實現。
它是一個插件對象,能夠對特定類型的節點進行處理,這裏咱們須要處理的節點是ArrowFunctionExpression
,它常見的配置方式有兩種:
一種是單一處理,結構以下,其中 path
表明當前遍歷的路徑 path.node
表明當前變量的節點
let visitor = {
ArrowFunctionExpression(path){
}
}
複製代碼
另外一種是用於輸入和輸出雙向處理,結構以下,參數 node
表示當前遍歷的節點
let visitor = {
ArrowFunctionExpression : {
enter(node){
},
leave(node){
}
}
}
複製代碼
這裏咱們只須要處理一次,因此採用第一種方式。
經過分析目標代碼的 AST
,咱們發現,須要一個 FunctionExpression
, 這時候咱們就須要用到 babel-types
,它的做用就是幫助咱們生產這些節點
咱們經過其 npm
包的文檔查看,構建一個 FunctionExpression
須要的參數以下:
參照 AST
咱們能夠看到其 id
爲 null
,params
是原 ArrowFunctionExpression
中的 params
,body
是一個blockStatement
,咱們也能夠經過查看 babel-types
文檔,用 t.blockStatement(body, directives)
來建立,依次類推,照貓畫虎,最終獲得的結果以下:
//原 params
let params = path.node.params;
//建立一個blockStatement
let blockStatement = types.blockStatement([
types.returnStatement(types.binaryExpression(
'+',
types.identifier('a'),
types.identifier('b')
))
]);
//建立一個函數
let func = types.functionExpression(null, params, blockStatement, false, false);
複製代碼
最後經過 path.replaceWith(func);
將其替換便可;
完成代碼以下:
//babel 核心庫,用來實現核心轉換引擎
const babel = require('@babel/core')
//類型判斷,生成AST零部件
const types = require('babel-types')
//源代碼
const code = `const sum=(a,b)=>a+b;` //目標代碼 const sum = function(a,b){ return a + b }
//插件對象,能夠對特定類型的節點進行處理
let visitor = {
//表明處理 ArrowFunctionExpression 節點
ArrowFunctionExpression(path){
let params = path.node.params;
//建立一個blockStatement
let blockStatement = types.blockStatement([
types.returnStatement(types.binaryExpression(
'+',
types.identifier('a'),
types.identifier('b')
))
]);
//建立一個函數
let func = types.functionExpression(null, params, blockStatement, false, false);
//替換
path.replaceWith(func);
}
}
//transform方法轉換code
//babel先將代碼轉換成ast,而後進行遍歷,最後輸出code
let result = babel.transform(code,{
plugins:[
{
visitor
}
]
})
console.log(result.code);
複製代碼
執行代碼,打印結果以下:
至此,咱們的函數轉換完成,達到預期效果。
怎麼樣,有沒有很神奇!!
其實也沒那麼複雜,主要是要分析其 AST
的結構,只要弄懂咱們須要幹什麼,那麼放手去作就是~
pass
:細心的同窗發現,上面的代碼其實能夠優化的,由於咱們的 returnStatement
其實也是跟源代碼的 returnStatement
是一致的,咱們只須要拿來複用便可,所以上述的代碼還能夠改爲下面這樣:
let blockStatement = types.blockStatement([
types.returnStatement(path.node.body)
]);
複製代碼
上面剛剛認識瞭如何使用 AST
進行代碼改造,不妨趁熱打鐵,再來試試下面這個問題。
如何將 es6
中的 class
改形成 es5
的 function
形式~
源代碼
// 源代碼
class Person {
constructor(name) {
this.name=name;
}
sayName() {
return this.name;
}
}
複製代碼
目標代碼
// 目標代碼
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
複製代碼
有了上面的基礎,照貓畫虎便可,這裏我將不在贅述,過程很重要,這裏我僅粘貼核心的轉換代碼,以供參考。
ClassDeclaration (path) {
let node = path.node; //當前節點
let id = node.id; //節點id
let methods = node.body.body; // 方法數組
let constructorFunction = null; // 構造方法
let functions = []; // 目標方法
methods.forEach(method => {
//若是是構造方法
if ( method.kind === 'constructor' ) {
constructorFunction = types.functionDeclaration(id, method.params, method.body, false, false);
functions.push(constructorFunction)
} else {
//普通方法
let memberExpression = types.memberExpression(types.memberExpression(id, types.identifier('prototype'), false), method.key, false);
let functionExpression = types.functionExpression(null, method.params, method.body, false, false);
let assignmentExpression = types.assignmentExpression('=', memberExpression, functionExpression);
functions.push(types.expressionStatement(assignmentExpression));
}
})
//判斷,replaceWithMultiple用於多重替換
if(functions.length === 1){
path.replaceWith(functions[0])
}else{
path.replaceWithMultiple(functions)
}
}
複製代碼
平常工做中,咱們大多數時候關注的只是 js
代碼自己,而沒有經過 AST
去從新認識和詮釋本身的代碼~
本文也只是經過對 AST
的一些介紹,起一個拋磚引玉的做用,讓你對它 有一個初步的認識,對它不在感受那麼陌生。
對代碼的追求和探索是無止境的~
若是你願意,你能夠經過它構建任何你想要的js
代碼~
加油!