抽象語法樹(Abstract Syntax Tree
)簡稱 AST
,是源代碼的抽象語法結構的樹狀表現形式。webpack
、eslint
等不少工具庫的核心都是經過抽象語法書這個概念來實現對代碼的檢查、分析等操做。今天我爲你們分享一下 JavaScript
這類解釋型語言的抽象語法樹的概念javascript
咱們經常使用的瀏覽器就是經過將 js
代碼轉化爲抽象語法樹來進行下一步的分析等其餘操做。因此將 js
轉化爲抽象語法樹更利於程序的分析。html
如上圖中變量聲明語句,轉換爲 AST
以後就是右圖中顯示的樣式vue
左圖中對應的:java
var
是一個關鍵字AST
是一個定義者=
是 Equal
等號的叫法有不少形式,在後面咱們還會看到is tree
是一個字符串;
就是 Semicoion
首先一段代碼轉換成的抽象語法樹是一個對象,該對象會有一個頂級的 type
屬性 Program
;第二個屬性是 body
是一個數組。node
body
數組中存放的每一項都是一個對象,裏面包含了全部的對於該語句的描述信息webpack
type: 描述該語句的類型 --> 變量聲明的語句
kind: 變量聲明的關鍵字 --> var
declaration: 聲明內容的數組,裏面每一項也是一個對象
type: 描述該語句的類型
id: 描述變量名稱的對象
type: 定義
name: 變量的名字
init: 初始化變量值的對象
type: 類型
value: 值 "is tree" 不帶引號
row: "\"is tree"\" 帶引號 複製代碼
JavaScript
是解釋型語言,通常經過 詞法分析 -> 語法分析 -> 語法樹,就能夠開始解釋執行了git
詞法分析:也叫掃描
,是將字符流轉換爲記號流(tokens
),它會讀取咱們的代碼而後按照必定的規則合成一個個的標識github
好比說:var a = 2
,這段代碼一般會被分解成 var
、a
、=
、2
;web
[
{ type: 'Keyword', value: 'var' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '2' },
]
複製代碼
當詞法分析源代碼的時候,它會一個一個字符的讀取代碼,因此很形象地稱之爲掃描 - scans
。當它遇到空格、操做符,或者特殊符號的時候,它會認爲一個話已經完成了。element-ui
語法分析:也稱解析器
,將詞法分析出來的數組轉換成樹的形式,同時驗證語法。語法若是有錯的話,拋出語法錯誤。
{
...
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
...
}
複製代碼
語法分析成 AST
,咱們能夠在這裏在線看到效果 esprima.org
語法檢查、代碼風格檢查、格式化代碼、語法高亮、錯誤提示、自動補全等 代碼混淆壓縮 優化變動代碼,改變代碼結構等 好比說,有個函數 function a() {}
我想把它變成 function b() {}
好比說,在 webpack
中代碼編譯完成後require('a') --> __webapck__require__("*/**/a.js")
下面來介紹一套工具,能夠把代碼轉成語法樹而後改變節點以及從新生成代碼
準備工具:
好比說一段代碼 function getUser() {}
,咱們把函數名字更改成 hello
,看代碼流程
看如下代碼,簡單說明 AST
遍歷流程
const esprima = require('esprima')
const estraverse = require('estraverse')
const code = `function getUser() {}`
// 生成 AST
const ast = esprima.parseScript(code)
// 轉換 AST,只會遍歷 type 屬性
// traverse 方法中有進入和離開兩個鉤子函數
estraverse.traverse(ast, {
enter(node) {
console.log('enter -> node.type', node.type)
},
leave(node) {
console.log('leave -> node.type', node.type)
},
})
複製代碼
輸出結果以下:
由此能夠獲得 AST 遍歷的流程是深度優先,遍歷過程以下:
此時咱們發現函數的名字在type
爲 Identifier
的時候就是該函數的名字,咱們就能夠直接修改它即可實現一個更改函數名字的 AST
工具
// 轉換樹
estraverse.traverse(ast, {
// 進入離開修改都是能夠的
enter(node) {
console.log('enter -> node.type', node.type)
if (node.type === 'Identifier') {
node.name = 'hello'
}
},
leave(node) {
console.log('leave -> node.type', node.type)
},
})
// 生成新的代碼
const result = escodegen.generate(ast)
console.log(result)
// function hello() {}
複製代碼
提到 AST
咱們確定會想到 babel
,自從 Es6 開始大規模使用以來,babel 就出現了,它主要解決了就是一些瀏覽器不兼容 Es6 新特性的問題,其實就把 Es6 代碼轉換爲 Es5 的代碼,兼容全部瀏覽器,babel 轉換代碼其實就是用了 AST,babel 與 AST 就有着很一種特別的關係。
那麼咱們就在 babel
的中來使用 AST
,看看 babel
是如何編譯代碼的(不講源碼啊)
須要用到兩個工具包 @babel/core、@babel/preset-env
當咱們配置 babel
的時候,不論是在 .babelrc
或者 babel.config.js
文件裏面配置的都有 presets
和 plugins
兩個配置項(還有其餘配置項,這裏不作介紹)
// .babelrc
{
"presets": ["@babel/preset-env"],
"plugins": []
}
複製代碼
當咱們配置了 presets
中有 @babel/preset-env
,那麼@babel/core
就會去找 preset-env
預設的插件包,它是一套
babel 核心包並不會去轉換代碼,核心包只提供一些核心 API,真正的代碼轉換工做由插件或者預設來完成,好比要轉換箭頭函數,會用到這個 plugin,@babel/plugin-transform-arrow-functions,當須要轉換的要求增長時,咱們不可能去一一配置相應的 plugin,這個時候就能夠用到預設了,也就是 presets。presets 是 plugins 的集合,一個 presets 內部包含了不少 plugin。
如今咱們有一個箭頭函數,要想把它轉成普通函數,咱們就能夠直接這麼寫:
const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b`
// babel 有 transform 方法會幫咱們自動遍歷,使用相應的預設或者插件轉換相應的代碼
const r = babel.transform(code, {
presets: ['@babel/preset-env'],
})
console.log(r.code)
// 打印結果以下
// "use strict";
// var fn = function fn() { return a + b; };
複製代碼
此時咱們能夠看到最終代碼會被轉成普通函數,可是咱們,只須要箭頭函數轉通函數的功能,不須要用這麼大一套包,只須要一個箭頭函數轉普通函數的包,咱們實際上是能夠在 node_modules
下面找到有個叫作 plugin-transform-arrow-functions
的插件,這個插件是專門用來處理 箭頭函數的,咱們就能夠這麼寫:
const r = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions'],
})
console.log(r.code)
// 打印結果以下
// const fn = function () { return a + b; };
複製代碼
咱們能夠從打印結果發現此時並無轉換咱們變量的聲明方式仍是 const
聲明,只是轉換了箭頭函數
此時,咱們就能夠本身來寫一些插件,來實現代碼的轉換,中間處理代碼的過程就是使用前面提到的 AST 的處理邏輯
如今咱們來個實戰把 const fn = (a, b) => a + b
轉換爲 const fn = function(a, b) { return a + b }
首先咱們在在線分析AST
的網站上分析 const fn = (a, b) => a + b
和const fn = function(a, b) { return a + b }
看二者語法樹的區別
根據咱們分析可得:
變成普通函數以後他就不叫箭頭函數了ArrowFunctionExpression
,而是函數表達式了 FunctionExpression
因此首先咱們要把 箭頭函數表達式 (ArrowFunctionExpression)
轉換爲 函數表達式(FunctionExpression)
要把 二進制表達式 (BinaryExpression)
放到一個 代碼塊中 (BlockStatement)
其實咱們要作就是把一棵樹變成另一顆樹,說白了其實就是拼成另外一顆樹的結構,而後生成新的代碼,就能夠完成代碼的轉換
在 babel 中
,咱們開發 plugins
的時候要用到訪問者模式,就是說在訪問到某一個路徑的時候進行匹配,而後在對這個節點進行修改,好比說上面的當咱們訪問到 ArrowFunctionExpression
的時候,對 ArrowFunctionExpression
進行修改,變成普通函數
那麼咱們就能夠這麼寫:
const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b` // 轉換後 const fn = function(a, b) { return a + b }
const arrowFnPlugin = {
// 訪問者模式
visitor: {
// 當訪問到某個路徑的時候進行匹配
ArrowFunctionExpression(path) {
// 拿到節點
const node = path.node
console.log('ArrowFunctionExpression -> node', node)
},
},
}
const r = babel.transform(code, {
plugins: [arrowFnPlugin],
})
console.log(r)
複製代碼
此時咱們拿到的結果是這樣的節點結果是 這樣的,其實就是 ArrowFunctionExpression
的 AST,此時咱們要作的是把 ArrowFunctionExpression
的結構替換成FunctionExpression
的結構,可是須要咱們組裝相似的結構,這麼直接寫很麻煩,可是 babel 爲咱們提供了一個工具叫作 @babel/types
@babel/types
有兩個做用:
判斷這個節點是否是這個節點(ArrowFunctionExpression 下面的 path.node 是否是一個 ArrowFunctionExpression) 生成對應的表達式 而後咱們使用的時候,須要常常查文檔,由於裏面的節點類型特別多,不是作編譯相關工做的是記不住怎麼多節點的
那麼接下來咱們就開始生成一個 FunctionExpression,而後把以前的ArrowFunctionExpression
替換掉,咱們能夠看 types 文檔,找到 functionExpression
,該方法接受相應的參數咱們傳遞過去便可生成一個 FunctionExpression
t.functionExpression(id, params, body, generator, async)
複製代碼
BlockStatement
,咱們接着看文檔找到 BlockStatement
接受的參數t.blockStatement(body, directives)
複製代碼
看文檔說明,blockStatement
接受一個 body,那咱們把以前的 body 拿過來就能夠直接用,不過這裏 body 接受一個數組
咱們細看 AST 結構,函數表達式中的 BlockStatement
中的 body 是一個 ReturnStatement
,因此咱們還須要生成一個 ReturnStatement
如今咱們就能夠改寫 AST 了
const babel = require('@babel/core')
const t = require('@babel/types')
const code = `const fn = (a, b) => a + b` // const fn = function(a, b) { return a + b }
const arrowFnPlugin = {
// 訪問者模式
visitor: {
// 當訪問到某個路徑的時候進行匹配
ArrowFunctionExpression(path) {
// 拿到節點而後替換節點
const node = path.node
console.log('ArrowFunctionExpression -> node', node)
// 拿到函數的參數
const params = node.params
const body = node.body
const functionExpression = t.functionExpression(
null,
params,
t.blockStatement([body])
)
// 替換原來的函數
path.replaceWith(functionExpression)
},
},
}
const r = babel.transform(code, {
plugins: [arrowFnPlugin],
})
console.log(r.code) // const fn = function (a, b) { return a + b; };
複製代碼
咱們知道在剪頭函數中是能夠省略 return 關鍵字,咱們上面是處理了省略關鍵字的寫法,可是若是用戶寫了 return 關鍵字後,咱們寫的這個插件就有問題了,因此咱們能夠在優化一下
const fn = (a, b) => { retrun a + b } -> const fn = function(a, b) { return a + b }
複製代碼
觀察代碼咱們發現,咱們就不須要把 body 轉換成 blockStatement 了,直接放過去就能夠了,那麼咱們就能夠這麼寫
ArrowFunctionExpression(path) {
// 拿到節點而後替換節點
const node = path.node
console.log("ArrowFunctionExpression -> node", node)
// 拿到函數的參數
const params = node.params
let body = node.body
// 判斷是否是 blockStatement,不是的話讓他變成 blockStatement
if (!t.isBlockStatement(body)) {
body = t.blockStatement([body])
}
const functionExpression = t.functionExpression(null, params, body)
// 替換原來的函數
path.replaceWith(functionExpression)
}
複製代碼
在開發中,咱們引入 UI 框架,好比 vue 中用到的 element-ui,vant 或者 React 中的 antd 都支持全局引入和按需引入,默認是全局引入,若是須要按需引入就須要安裝一個 babel-plugin-import 的插件,將全局的寫法變成按需引入的寫法。
就拿我最近開發移動端用的 vant 爲例, import { Button } from 'vant'
這種寫法通過這個插件以後會變成import Button from 'vant/lib/Button
' 這種寫法,引用整個 vant 變成了我只用了 vant 下面的某一個文件,打包後的文件會比所有引入的文件大小要小不少
import { Button, Icon } from 'vant'
寫法轉換爲 import Button from 'vant/lib/Button'
; import Icon from 'vant/lib/Icon'
看一下兩個語法樹的區別
根據兩張圖分析咱們能夠獲得一些信息:
咱們發現解構方式引入的模塊只有 import 聲明,第二張圖是兩個 import 聲明 解構方式引入的詳細說明裏面(specifiers
)是兩個 ImportSpecifier
,第二張圖裏面是分開的,並且都是 ImportDefaultSpecifier
他們引入的 source
也不同 那咱們要作的其實就是要把單個的 ImportDeclaration
變成多個ImportDeclaration
, 而後把單個import
解構引入的 specifiers
部分 ImportSpecifier
轉換成多個 ImportDefaultSpecifier
並修改對應的 source
便可
爲了方便傳遞參數,此次咱們寫到一個函數裏面,能夠方便傳遞轉換後拼接的目錄
這裏咱們須要用到的幾個類型,也須要在 types 官網上找對應的解釋
首先咱們要生成多個 importDeclaration
類型
/** * @param {Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>} specifiers (required) * @param {StringLiteral} source (required) */
t.importDeclaration(specifiers, source)
複製代碼
在 importDeclaration 中須要生成 ImportDefaultSpecifier
/** * @param {Identifier} local (required) */
t.importDefaultSpecifier(local)
複製代碼
在 importDeclaration 中還須要生成一個 StringLiteral
/** * @param {string} value (required) */
t.stringLiteral(value)
複製代碼
按照上面的分析,咱們開始上代碼
const babel = require('@babel/core')
const t = require('@babel/types')
const code = `import { Button, Icon } from 'vant'`
// import Button from 'vant/lib/Button'
// import Icon from 'vant/lib/Icon'
function importPlugin(opt) {
const { libraryDir } = opt
return {
visitor: {
ImportDeclaration(path) {
const node = path.node
// console.log("ImportDeclaration -> node", node)
// 獲得節點的詳細說明,而後轉換成多個的 import 聲明
const specifiers = node.specifiers
// 要處理這個咱們作一些判斷,首先判斷不是默認導出咱們才處理,要考慮 import vant, { Button, Icon } from 'vant' 寫法
// 還要考慮 specifiers 的長度,若是長度不是 1 而且不是默認導出咱們才須要轉換
if (
!(
specifiers.length === 1 && t.isImportDefaultSpecifier(specifiers[0])
)
) {
const result = specifiers.map((specifier) => {
const local = specifier.local
const source = t.stringLiteral(
`${node.source.value}/${libraryDir}/${specifier.local.name}`
)
// console.log("ImportDeclaration -> specifier", specifier)
return t.importDeclaration(
[t.importDefaultSpecifier(local)],
source
)
})
console.log('ImportDeclaration -> result', result)
// 由於此次要替換的 AST 不是一個,而是多個的,因此須要 `path.replaceWithMultiple(result)` 來替換,可是一執行發現死循環了
path.replaceWithMultiple(result)
}
},
},
}
}
const r = babel.transform(code, {
plugins: [importPlugin({ libraryDir: 'lib' })],
})
console.log(r.code)
複製代碼
看打印結果和轉換結果彷佛沒什麼問題,這個插件幾乎就實現了
可是咱們考慮一種狀況,若是用戶不所有按需加載了,按需加載只是一種選擇,若是用戶這麼寫了 import vant, { Button, Icon } from 'vant'
,那麼咱們這個插件就出現問題了
若是遇到這種寫法,那麼默認導入的他的 source 應該是不變的,咱們要把原來的 source 拿出來
因此還須要判斷一下,每個 specifier 是否是一個 ImportDefaultSpecifier 而後處理不一樣的 source,完整處理邏輯應該以下
function importPlugin(opt) {
const { libraryDir } = opt
return {
visitor: {
ImportDeclaration(path) {
const node = path.node
// console.log("ImportDeclaration -> node", node)
// 獲得節點的詳細說明,而後轉換成多個的 import 聲明
const specifiers = node.specifiers
// 要處理這個咱們作一些判斷,首先判斷不是默認導出咱們才處理,要考慮 import vant, { Button, Icon } from 'vant' 寫法
// 還要考慮 specifiers 的長度,若是長度不是 1 而且不是默認導出咱們才須要轉換
if (
!(
specifiers.length === 1 && t.isImportDefaultSpecifier(specifiers[0])
)
) {
const result = specifiers.map((specifier) => {
let local = specifier.local,
source
// 判斷是否存在默認導出的狀況
if (t.isImportDefaultSpecifier(specifier)) {
source = t.stringLiteral(node.source.value)
} else {
source = t.stringLiteral(
`${node.source.value}/${libraryDir}/${specifier.local.name}`
)
}
return t.importDeclaration(
[t.importDefaultSpecifier(local)],
source
)
})
path.replaceWithMultiple(result)
}
},
},
}
}
複製代碼
在 babel 官網上有一句話Babylon is a JavaScript parser used in Babel.
babel 使用的引擎是 babylon,Babylon 並不是 babel 團隊本身開發的,而是 fork 的 acorn 項目,acorn 的項目本人在很早以前在興趣部落 1.0 在構建中使用,爲了是作一些代碼的轉換,是很不錯的一款引擎,不過 acorn 引擎只提供基本的解析 ast 的能力,遍歷還須要配套的 acorn-travesal, 替換節點須要使用 acorn-,而這些開發,在 Babel 的插件體系開發下,變得一體化了(摘自 AlloyTeam 團隊的剖析 babel)
使用 babylon
編寫一個數組 rest 轉 Es5 語法的插件
把 const arr = [ ...arr1, ...arr2 ]
轉成 var arr = [].concat(arr1, arr2)
咱們使用 babylon 的話就不須要使用@babel/core
了,只須要用到他裏面的 traverse
和generator
,用到的包有 babylon、@babel/traverse、@babel/generator、@babel/types
先來看一下兩棵語法樹的區別
根據上圖咱們分析得出:
兩棵樹都是變量聲明的方式,不一樣的是他們聲明的關鍵字不同 他們初始化變量值的時候是不同的,一個數組表達式(ArrayExpression)另外一個是調用表達式(CallExpression) 那咱們要作的就很簡單了,就是把 數組表達式轉換爲調用表達式就能夠
這段代碼的核心生成一個 callExpression 調用表達式,因此對應官網上的類型,咱們分析須要用到的 api
先來分析 init 裏面的,首先是 callExpression
/** * @param {Expression} callee (required) * @param {Array<Expression | SpreadElement | JSXNamespacedName>} source (required) */
t.callExpression(callee, arguments)
複製代碼
對應語法樹上 callee 是一個 MemberExpression,因此要生成一個成員表達式
/** * @param {Expression} object (required) * @param {if computed then Expression else Identifier} property (required) * @param {boolean} computed (default: false) * @param {boolean} optional (default: null) */
t.memberExpression(object, property, computed, optional)
複製代碼
在 callee 的 object 是一個 ArrayExpression 數組表達式,是一個空數組
/** * @param {Array<null | Expression | SpreadElement>} elements (default: []) */
t.arrayExpression(elements)
複製代碼
對了裏面的東西分析完了,咱們還要生成 VariableDeclarator 和 VariableDeclaration 最終生成新的語法樹
/** * @param {LVal} id (required) * @param {Expression} init (default: null) */
t.variableDeclarator(id, init)
/** * @param {"var" | "let" | "const"} kind (required) * @param {Array<VariableDeclarator>} declarations (required) */
t.variableDeclaration(kind, declarations)
複製代碼
其實倒着分析語法樹,分析完怎麼寫也就清晰了,那麼咱們開始上代碼吧
const babylon = require('babylon')
// 使用 babel 提供的包,traverse 和 generator 都是被暴露在 default 對象上的
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
const code = `const arr = [ ...arr1, ...arr2 ]` // var arr = [].concat(arr1, arr2)
const ast = babylon.parse(code, {
sourceType: 'module',
})
// 轉換樹
traverse(ast, {
VariableDeclaration(path) {
const node = path.node
const declarations = node.declarations
console.log('VariableDeclarator -> declarations', declarations)
const kind = 'var'
// 邊界斷定
if (node.kind !== kind && declarations.length === 1 && t.isArrayExpression(declarations[0].init)) {
// 取得以前的 elements
const args = declarations[0].init.elements.map((item) => item.argument)
const callee = t.memberExpression(t.arrayExpression(), t.identifier('concat'), false)
const init = t.callExpression(callee, args)
const declaration = t.variableDeclarator(declarations[0].id, init)
const variableDeclaration = t.variableDeclaration(kind, [declaration])
path.replaceWith(variableDeclaration)
}
},
})
複製代碼
和抽象語法樹相對的是具體語法樹(Concrete Syntax Tree)簡稱 CST(一般稱做分析樹)。通常的,在源代碼的翻譯和編譯過程當中,語法分析器建立出分析樹。一旦 AST 被建立出來,在後續的處理過程當中,好比語義分析階段,會添加一些信息。可參考抽象語法樹和具體語法樹有什麼區別?
關於 node 類型,全集大體以下:
(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration |
FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator |
ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement |
ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement |
SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement |
ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration |
ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression |
UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression |
ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral |
TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property |
AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern |
RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration |
ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier |
ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier
複製代碼
Babel
有文檔對 AST
樹的詳細定義,可參考這裏