什么是抽象語法樹?
在計算機科學中,抽象語法和抽象語法樹其實是源代碼的抽象語法結構的樹狀表現形式
在線編輯器
我們常用的瀏覽器就是通過將js代碼轉化為抽象語法樹來進行下一步的分析等其他操作。所以將js轉化為抽象語法樹更利於程序的分析。
如圖:
如上圖中的變量聲明語句,轉化為AST之后就是右圖的樣子。
先來分析一下左圖:
var 是一個關鍵字
AST是一個定義者
= 是Equal 等號的叫法有很多形式,在后面我們還會看到
“is tree” 是一個字符串
;就是 Semicoion
再來對應一下右圖:
首先一段代碼轉化成的抽象語法樹是一個對象,該對象會有一個頂級的type屬性'Program',第二個屬性是body是一個數組。
body數組中存放的每一項都是一個對象,里面包含了所有的對於該語句的描述信息
type:描述該語句的類型 --變量聲明語句
kind:變量聲明的關鍵字 -- var
declaration: 聲明的內容數組,里面的每一項也是一個對象
type: 描述該語句的類型
id: 描述變量名稱的對象
type:定義
name: 是變量的名字
init: 初始化變量值得對象
type: 類型
value: 值 "is tree" 不帶引號
row: "\"is tree"\" 帶引號
抽象語法樹有哪些用途?
代碼語法的檢查,代碼風格的檢查,代碼的格式化,代碼的高亮,代碼錯誤提示,代碼自動補全等等
如:JSLint、JSHint 對代碼錯誤或風格的檢查,發現一些潛在的錯誤
IDE的錯誤提示,格式化,高亮,自動補全等等
代碼的混淆壓縮
如:UglifyJS2等
優化變更代碼,改變代碼結構達到想要的結構
代碼打包工具webpack,rollup等等
CommonJS、AMD、CMD、UMD等代碼規范之間的轉化
CoffeeScript、TypeScript、JSX等轉化為原生Javascript
通過什么工具或庫來實現源碼轉化為抽象語法樹?
那就是javascript Parser 解析器,他會把js源碼轉化為抽象的語法樹。
瀏覽器會把js源碼通過解析器轉化為抽象語法樹,再進一步轉化為字節碼或直接生成機器碼
一般來說每一個js引擎都會有自己的抽象語法樹格式,chrome的v8引擎,firefox的SpiderMonkey 引擎等等,MDN提供了詳細SpiderMonkey AST format的詳細說明,算是業界的標准。(SpiderMonkey是Mozilla項目的一部分,是一個用C語言實現的JavaScript腳本引擎,為了在SpiderMonkey中運行JavaScript代碼,應用程序必須有三個要素:JSRuntime,JSContext和全局對象。)
常用的javascript Parser
esprima
traceur
acorn
shift
我們主要拿esprima來舉一個例子
安裝
npm install esprima estraverse escodegen -S
esprima 涉及三個庫名稱和功能如下:
esprima 把源碼轉化為抽象語法樹
let esprima = require('esprima'); // 引入esprima
let jsOrigin = 'function eat(){};'; // 定義一個js源碼
let AST = esprima.parse(jsOrigin); // 通過esprima.parse將js源碼轉化為一個抽象語法樹
console.log(AST); // 打印生成的抽象語法樹
/*Script {
type: 'Program',// 頂級的type屬性
body: [ FunctionDeclaration {
type: 'FunctionDeclaration', // js源碼的類型--是一個函數聲明
id: [Identifier],
params: [],
body: [BlockStatement],
generator: false, // 是不是generator函數
expression: false, // 是不是一個表達式
async: false // 是不是一個異步函數
},
EmptyStatement { type: 'EmptyStatement' }
],
sourceType: 'script'
}*/
estraverse 遍歷並更新抽象語法樹
在介紹用法之前我們先來npm上看一下這個庫,這個庫的下載量居然500多萬,而且沒有README說明文檔,是不是很牛掰!
在舉例子之前我們要遍歷抽象語法樹,首先我們要先了解一下他的遍歷順利
let estraverse = require('estraverse');
estraverse.traverse(AST, {
enter(node){
console.log('enter', node.type)
if(node.type === 'Identifier') {
node.name += '_enter'
}
},
leave(node){
console.log('leave', node.type)
if(node.type === 'Identifier') {
node.name += '_leave'
}
}
})
// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// enter EmptyStatement
// leave EmptyStatement
// leave Program
通過上面節點類型的打印結果我們不難看出,我們的抽象語法樹的每個節點被訪問了2次,一次是進入的時候,一次是離開的時候,我們可以通過下面的圖來更加清楚的理解抽象語法樹的遍歷順序
看完遍歷順序之后,我們看到代碼中的判斷條件 如果是變量名的話,第一次進入訪問時對這個變量的名稱做了一次修改,當離開的時候也做了一次修改。那接下來我們要驗證 抽象語法樹種的這個節點的變量名稱 是否修改成功了呢?我們有兩種方案,方案一:直接打印抽象語法樹,這個非常簡單再這里就你介紹了。方案二: 我們將現有的抽象語法樹轉化成源碼看一下變量名是否變成功 這樣就一目了然了。那怎么將我們的抽象語法樹還原成源碼呢?這就要引入我們的第三個庫了 escodegen
escodegen 將抽象語法樹還原成js源碼
let escodegen = require('escodegen');
let originReback = escodegen.generate(AST);
console.log(originReback);
// function eat_enter_leave() {};
通過上面還原回來的源碼我們看到變量名稱確實被更改了。
接下來我們來探索一下如何用抽象語法樹來將箭頭函數轉化為普通的函數
我們都知道es6語法轉es5的語法我們用的是babel,讓我們接下來就看一下 babel是如何將箭頭函數轉化為普通函數的。
第一步需要使用babel的兩個插件,babel-core 核心模塊 babel-types 類型模塊
npm i babel-core babel-types -S
第一步:我們先來對比普通函數和箭頭函數的抽象語法樹,通過對比找出其中的不同之處,然后在節點可以復用的前提下,盡可能少的改變一下不同的地方,從而成功的將箭頭函數轉化為普通函數。
我們以這個箭頭函數為例:
let sum = (a,b) => a+b;
------>
var sum = function sum(a, b) {
return a + b;
};
如上圖所示,普通函數和箭頭函數的AST的不同在於init,所以我們現在要做的是將箭頭函數的arrowFunctionExpression 轉換為FunctionExpression
利用babel-types生成新的部分的AST語法樹,替換原有的。如果創建某個節點的語法樹,那就在下面的網址上,需要哪個節點就搜哪個節點
babel-types
// babel 核心庫,用來實現核心的轉換引擎
const babel = require('babel-core');
// 實現類型轉化 生成AST節點
const types = require('babel-types');
let code = 'let sum = (a,b) => a+b;';
let es5Code = function (a,b) {
return a+b;
};
// babel 轉化采用的是訪問者模式Visitor 對於某個對象或者一組對象,不同的訪問者,產生的結果不同,執行操作也不同
// 這個訪問者可以對特定的類型的節點進行處理
let visitor = {
ArrowFunctionExpression(path) {
// 如果這個節點是箭頭函數的節點的話,我們在這里進行處理替換工作
// 1.復用params參數
let params = path.node.params;
let blockStatement = types.blockStatement([types.returnStatement(path.node.body)])
let func = types.functionExpression(null, params, blockStatement, false,false);
path.replaceWith(func)
}
};
let arrayPlugin = {visitor};
// babel內部先把代碼轉化成AST,然后進行遍歷
let result = babel.transform(code, {
plugins: [
arrayPlugin
]
});
console.log(result.code);
// let sum = function (a, b) {
// return a + b;
// };
我們寫一個babel的預計算插件
let code = `const result = 1000 * 60 * 60 * 24`;
//let code = `const result = 1000 * 60`;
let babel = require('babel-core');
let types = require('babel-types');
//預計算
let visitor = {
BinaryExpression(path){
let node = path.node;
if(!isNaN(node.left.value)&&!isNaN(node.right.value)){
let result = eval(node.left.value+node.operator+node.right.value);
result = types.numericLiteral(result);
path.replaceWith(result);
//如果此表達式的父親也是一個表達式的話,需要遞歸計算
if(path.parentPath.node.type == 'BinaryExpression'){
visitor.BinaryExpression.call(null,path.parentPath);
}
}
}
}
let r = babel.transform(code,{
plugins:[
{visitor}
]
});
console.log(r.code);
以上就是我對抽象語法樹的理解,有什么不正確的地方,懇求斧正。