什么是抽象語法樹(Abstract Syntax Tree ,AST)?
百度百科是這么解釋的:
在計算機(jī)科學(xué)中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
聽起來還是很繞,沒關(guān)系,你可以簡單理解為 它就是你所寫代碼的的樹狀結(jié)構(gòu)化表現(xiàn)形式。
有了這棵樹,我們就可以通過操縱這顆樹,精準(zhǔn)的定位到聲明語句、賦值語句、運(yùn)算語句等等,實(shí)現(xiàn)對代碼的分析、優(yōu)化、變更等操作。
AST在日常業(yè)務(wù)中也許很難涉及到,有可能你還沒有聽過,但其實(shí)很多時候你已經(jīng)在使用它了,只是沒有太多關(guān)注而已,現(xiàn)在流行的 webpack,eslint 等很多插件或者包都有涉及~
抽象語法樹能做什么?
聊到AST的用途,其應(yīng)用非常廣泛,下面我簡單羅列了一些:
-
IDE的錯誤提示、代碼格式化、代碼高亮、代碼自動補(bǔ)全等 -
JSLint、JSHint對代碼錯誤或風(fēng)格的檢查等 -
webpack、rollup進(jìn)行代碼打包等 -
CoffeeScript、TypeScript、JSX等轉(zhuǎn)化為原生Javascript
其實(shí)它的用途,還不止這些,如果說你已經(jīng)不滿足于實(shí)現(xiàn)枯燥的業(yè)務(wù)功能,想寫出類似react、vue這樣的牛逼框架,或者想自己搞一套類似webpack、rollup這樣的前端自動化打包工具,那你就必須弄懂AST。
如何生成AST?
在了解如何生成AST之前,有必要了解一下Parser(常見的Parser有esprima、traceur、acorn、shift等)
JS Parser其實(shí)是一個解析器,它是將js源碼轉(zhuǎn)化為抽象語法樹(AST)的解析器。
整個解析過程主要分為以下兩個步驟:
- 分詞:將整個代碼字符串分割成最小語法單元數(shù)組
- 語法分析:在分詞基礎(chǔ)上建立分析語法單元之間的關(guān)系
什么是語法單元?
語法單元是被解析語法當(dāng)中具備實(shí)際意義的最小單元,簡單的來理解就是自然語言中的詞語。
舉個例子來說,下面這段話:
“2019年是祖國70周年”
我們可以把這句話拆分成最小單元,即:2019年、是、祖國、70、周年。
這就是我們所說的分詞,也是最小單元,因?yàn)槿绻覀儼阉俨鸱殖鋈サ脑?,那就沒有什么實(shí)際意義了。
Javascript 代碼中的語法單元主要包括以下這么幾種:
- 關(guān)鍵字:例如
var、let、const等 - 標(biāo)識符:沒有被引號括起來的連續(xù)字符,可能是一個變量,也可能是
if、else這些關(guān)鍵字,又或者是true、false這些內(nèi)置常量 - 運(yùn)算符:
+、-、*、/等 - 數(shù)字:像十六進(jìn)制,十進(jìn)制,八進(jìn)制以及科學(xué)表達(dá)式等語法
- 字符串:因?yàn)閷τ嬎銠C(jī)而言,字符串的內(nèi)容會參與計算或顯示
- 空格:連續(xù)的空格,換行,縮進(jìn)等
- 注釋:行注釋或塊注釋都是一個不可拆分的最小語法單元
- 其他:大括號、小括號、分號、冒號等
如果我們以最簡單的復(fù)制語句為例的話,如下?
var a = 1;
通過分詞,我們可以得到如下結(jié)果:
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": ";"
}
]
為了方便,我直接在 esprima/parser 這個網(wǎng)站生成分詞的~
什么是語法分析?
上面我們已經(jīng)得到了我們分詞的結(jié)果,需要將詞匯進(jìn)行一個立體的組合,確定詞語之間的關(guān)系,確定詞語最終的表達(dá)含義。
簡單來說語法分析是對語句和表達(dá)式識別,確定之前的關(guān)系,這是個遞歸過程。
上面我們通過語法分析,可以得到如下結(jié)果:
{
"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 所轉(zhuǎn)換的 AST;
這里推薦一下astexplorer AST的可視化工具,astexplorer,可以直接進(jìn)行對代碼進(jìn)行AST轉(zhuǎn)換~
AST 到底怎么用?
上面畫了很大篇幅聊了聊AST是什么以及它是如何生成的,說到底,還是不知道AST這玩意有啥用,到底怎么使用。。
ok~ 接下來我們來一起見證奇跡。
我相信大部分同學(xué)對 babel 這個庫不陌生,現(xiàn)在的做前端模塊化開發(fā)過程中中一定少不了它,因?yàn)樗梢詭湍銓?ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中,你不用為新語法的兼容性考慮~
而實(shí)際上呢,babel 中的很多功能都是靠修改 AST 實(shí)現(xiàn)的。
首先,我們來看一個簡單的例子,我們?nèi)绾螌?es6 中的 箭頭函數(shù) 轉(zhuǎn)換成 es5 中的 普通函數(shù),即:
const sum=(a,b)=>a+b;
我們?nèi)绾螌⑸厦婧唵蔚?sum 箭頭函數(shù)轉(zhuǎn)換成下面的形式:
const sum = function(a,b){
return a+b;
}
想想看,有什么思路?
如果說你不了解 AST 的話,這無疑是一個很困難的問題,根本無從下手,如果你了解 AST 的話,這將是一個非常 easy 的例子。
接下來我們看看如何實(shí)現(xiàn)?
安裝依賴
需要操作 AST 代碼,這里,我們需要借助兩個庫,分別是 @babel/core 和 babel-types。
其中 @babel/core 是 babel 的核心庫,用來實(shí)現(xiàn)核心轉(zhuǎn)換引擎,babel-types 類型判斷,用于生成AST零部件
cd 到一個你喜歡的目錄,通過 npm -y init 初始化操作后,通過 npm i @babel/core babel-types -D 安裝依賴
目標(biāo)分析
要進(jìn)行轉(zhuǎn)換之前,我們需要進(jìn)行分析,首先我們先通過 astexplorer 查看目標(biāo)代碼跟我們現(xiàn)在的代碼有什么區(qū)別。
源代碼的 AST 結(jié)構(gòu)如下:
// 源代碼的 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"
}
目標(biāo)代碼的 AST 結(jié)構(gòu)如下:
// 目標(biāo)代碼的 `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 我們不用在意,其只是為了標(biāo)記其所在代碼的位置。
我們關(guān)心的是 body 里面的內(nèi)容,通過對比,我們發(fā)現(xiàn)主要不同就在于 init 這一段,一個是 ArrowFunctionExpression , 另一個是 FunctionExpression , 我們只需要將 ArrowFunctionExpression 下的內(nèi)容改造成跟 FunctionExpression 即可。
小試牛刀
我們建一個 arrow.js 的文件,引入上面的兩個庫,即
//babel 核心庫,用來實(shí)現(xiàn)核心轉(zhuǎn)換引擎
const babel = require('@babel/core')
//類型判斷,生成AST零部件
const types = require('babel-types')
//源代碼
const code = `const sum=(a,b)=>a+b;` //目標(biāo)代碼 const sum = function(a,b){ return a + b }
這里我們需要用到 babel 中的 transform 方法,它可以將 js 代碼轉(zhuǎn)換成 AST ,過程中可以通過使用 plugins 對 AST 進(jìn)行改造,最終生成新的 AST 和 js 代碼,其整個過程用網(wǎng)上一個比較貼切的圖就是:

關(guān)于 babel transform 詳細(xì)用法,這里不多做討論,感興趣的話可以去官網(wǎng)了解~
其主要用法如下:
//transform方法轉(zhuǎn)換code
//babel先將代碼轉(zhuǎn)換成ast,然后進(jìn)行遍歷,最后輸出code
let result = babel.transform(code,{
plugins:[
{
visitor
}
]
})
其核心在于插件 visitor 的實(shí)現(xiàn)。
它是一個插件對象,可以對特定類型的節(jié)點(diǎn)進(jìn)行處理,這里我們需要處理的節(jié)點(diǎn)是ArrowFunctionExpression,它常見的配置方式有兩種:
一種是單一處理,結(jié)構(gòu)如下,其中 path 代表當(dāng)前遍歷的路徑 path.node 代表當(dāng)前變量的節(jié)點(diǎn)
let visitor = {
ArrowFunctionExpression(path){
}
}
另一種是用于輸入和輸出雙向處理,結(jié)構(gòu)如下,參數(shù) node 表示當(dāng)前遍歷的節(jié)點(diǎn)
let visitor = {
ArrowFunctionExpression : {
enter(node){
},
leave(node){
}
}
}
這里我們只需要處理一次,所以采用第一種方式。
通過分析目標(biāo)代碼的 AST,我們發(fā)現(xiàn),需要一個 FunctionExpression , 這時候我們就需要用到 babel-types ,它的作用就是幫助我們生產(chǎn)這些節(jié)點(diǎn)
我們通過其 npm 包的文檔查看,構(gòu)建一個 FunctionExpression 需要的參數(shù)如下:

參照 AST 我們可以看到其 id 為 null,params 是原 ArrowFunctionExpression 中的 params,body 是一個blockStatement,我們也可以通過查看 babel-types 文檔,用 t.blockStatement(body, directives) 來創(chuàng)建,依次類推,照貓畫虎,最終得到的結(jié)果如下:
//原 params
let params = path.node.params;
//創(chuàng)建一個blockStatement
let blockStatement = types.blockStatement([
types.returnStatement(types.binaryExpression(
'+',
types.identifier('a'),
types.identifier('b')
))
]);
//創(chuàng)建一個函數(shù)
let func = types.functionExpression(null, params, blockStatement, false, false);
最后通過 path.replaceWith(func); 將其替換即可;
完成代碼如下:
//babel 核心庫,用來實(shí)現(xiàn)核心轉(zhuǎn)換引擎
const babel = require('@babel/core')
//類型判斷,生成AST零部件
const types = require('babel-types')
//源代碼
const code = `const sum=(a,b)=>a+b;` //目標(biāo)代碼 const sum = function(a,b){ return a + b }
//插件對象,可以對特定類型的節(jié)點(diǎn)進(jìn)行處理
let visitor = {
//代表處理 ArrowFunctionExpression 節(jié)點(diǎn)
ArrowFunctionExpression(path){
let params = path.node.params;
//創(chuàng)建一個blockStatement
let blockStatement = types.blockStatement([
types.returnStatement(types.binaryExpression(
'+',
types.identifier('a'),
types.identifier('b')
))
]);
//創(chuàng)建一個函數(shù)
let func = types.functionExpression(null, params, blockStatement, false, false);
//替換
path.replaceWith(func);
}
}
//transform方法轉(zhuǎn)換code
//babel先將代碼轉(zhuǎn)換成ast,然后進(jìn)行遍歷,最后輸出code
let result = babel.transform(code,{
plugins:[
{
visitor
}
]
})
console.log(result.code);
執(zhí)行代碼,打印結(jié)果如下:

至此,我們的函數(shù)轉(zhuǎn)換完成,達(dá)到預(yù)期效果。
怎么樣,有沒有很神奇!!
其實(shí)也沒那么復(fù)雜,主要是要分析其 AST 的結(jié)構(gòu),只要弄懂我們需要干什么,那么放手去做就是~
pass:細(xì)心的同學(xué)發(fā)現(xiàn),上面的代碼其實(shí)可以優(yōu)化的,因?yàn)槲覀兊?returnStatement 其實(shí)也是跟源代碼的 returnStatement 是一致的,我們只需要拿來復(fù)用即可,因此上述的代碼還可以改成下面這樣:
let blockStatement = types.blockStatement([
types.returnStatement(path.node.body)
]);
趁熱打鐵
上面剛剛認(rèn)識了如何使用 AST 進(jìn)行代碼改造,不妨趁熱打鐵,再來試試下面這個問題。
如何將 es6 中的 class 改造成 es5 的 function 形式~
源代碼
// 源代碼
class Person {
constructor(name) {
this.name=name;
}
sayName() {
return this.name;
}
}
目標(biāo)代碼
// 目標(biāo)代碼
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
有了上面的基礎(chǔ),照貓畫虎即可,這里我將不在贅述,過程很重要,這里我僅粘貼核心的轉(zhuǎn)換代碼,以供參考。
ClassDeclaration (path) {
let node = path.node; //當(dāng)前節(jié)點(diǎn)
let id = node.id; //節(jié)點(diǎn)id
let methods = node.body.body; // 方法數(shù)組
let constructorFunction = null; // 構(gòu)造方法
let functions = []; // 目標(biāo)方法
methods.forEach(method => {
//如果是構(gòu)造方法
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)
}
}
總結(jié)
日常工作中,我們大多數(shù)時候關(guān)注的只是 js 代碼本身,而沒有通過 AST 去重新認(rèn)識和詮釋自己的代碼~
本文也只是通過對 AST 的一些介紹,起一個拋磚引玉的作用,讓你對它 有一個初步的認(rèn)識,對它不在感覺那么陌生。
對代碼的追求和探索是無止境的~
如果你愿意,你可以通過它構(gòu)建任何你想要的js代碼~
加油!