wiki: 抽象語法樹( Abstract Syntax Tree,AST ),或簡稱語法樹( Syntax tree ),是源代碼語法結構的一種抽象表示。 它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。 之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。前端
在咱們前端,能夠經過 Javascript
解析器將咱們程序的源代碼映射成爲一棵語法樹,而樹的每一個節點對應着代碼裏的一種結構;好比表達式,聲明語句,賦值語句等都會被映射爲語法樹上的一個節點,進而咱們就能夠經過操做語法樹上的節點來控制咱們的源代碼;初步瞭解時可能感受這個概念自己就比較抽象,可是基於它的應用咱們卻一點都不陌生,好比:單文件組件 .vue
文件的解析、爲咱們轉碼 ES6
語法的 babeljs
、爲咱們壓縮混淆代碼的 UglifyJS
等等;vue
Babel is a JavaScript compiler. Use next generation JavaScript, today.node
Babel
是一個Javascript
編譯器,他能讓你如今就開始使用將來版的Javascript
。曾經的Javascript
因爲語言自己的設計缺陷,飽受程序員們的詬病,現在隨着ES
語言規範的制定與發展,加上Typescript
的橫空出世,Javascript
開始逐年霸佔最流行語言的榜首;Babel
是推進這一進程的重要推手,他能將ES6/7/8/9/10
轉換爲瀏覽器可以兼容的Javascript
,從如今開始,您就可使用最新的ES
語法規範,甚至處於草案階段的規範;而這都是一切都是史無前例的!程序員
接下來咱們將使用Babel
提供的相關AST
操做的模塊來「改頭換面」咱們的代碼;編程
@babel/parser
經過該模塊來解析咱們的代碼生成AST
抽象語法樹;瀏覽器
@babel/traverse
經過該模塊對AST
節點進行遞歸遍歷;babel
@babel/types
經過該模塊對具體的AST
節點進行進行增、刪、改、查;app
@babel/generator
經過該模塊能夠將修改後的AST
生成新的代碼;框架
Talk is cheap, show me your code!編程語言
如今假設咱們在代碼裏使用了一個叫log
的方法,如:log('Hello, world!');
,可是咱們並無聲明或定義該方法,通常狀況下咱們的代碼將會報一個log is not defined
的錯誤,如今咱們經過操做AST
將log
修改成console.log
,這樣咱們的代碼就不會報錯了;
在astexplorer.net
網站裏咱們能夠在線將代碼轉爲AST
,以下圖:log('Hello, world!')
的AST
樹形結構圖;
通常狀況下,咱們從body
層級看起,其下的每一層級都有一個type
字段,該字段很是重要,直接影響咱們該如何對該語句或表達式進行操做,具體請看後面講到的@babel/types
;
拿到代碼,咱們首先經過@babel/parser
生成如上圖所示結構的AST
:
const {parse} = require('@babel/parser');
const codes = "log('Hello, world!');";
const ast = parse(codes, {
sourceType: "module"
});
複製代碼
對照AST
分析咱們須要作什麼,新手強烈推薦使用astexplorer.net
,好比在這裏,咱們的需求是將log
函數轉換爲console.log
,經過在線AST
解析,咱們清楚的看到了log
對應的AST
節點類型爲Identifier
,一樣的,咱們換成console.log('Hello, world!');
,能夠看到console.log
對應的AST
節點類型爲MemberExpression
,因此,咱們的需求變爲將此處的Identifier
變爲MemberExpression
,不要想固然的覺得直接把type
屬性改個值就 ok 了,接下來看看,如何將Identifier
類型的節點修改成MemberExpression
類型的節點;
輪到@babel/types
上場了,前面兩步,咱們的焦點主要在AST
節點的type
字段上,事實上,每個type
在@babel/types
裏都有一個同名的方法(首字母小寫)用來建立該類型的節點,好比建立Identifier
類型的節點,咱們可使用t.identifier
方法;
好了,先來建立MemberExpression
類型的console.log
,此處對於新手會比較棘手,推薦好好觀察在線的AST
樹進行反推,找到目標節點的type
,接着在@babel/types
文檔裏搜相應type
的方法;如圖,咱們在文檔裏搜到memberExpression
方法的定義,接着開始建立console.log
節點;
const t = require('@babel/types');
function createMemberExpression() {
return t.memberExpression(
t.identifier('console'),
t.identifier('log')
);
}
複製代碼
如上,咱們就能夠經過createMemberExpression
方法來生成console.log
來替換log
了,問題來了,如何替換?
固然,替換以前,咱們須要在AST
樹上找到對應的節點,經過@babel/traverse
咱們能夠對AST
樹的節點進行遍歷;
const {default: traverse} = require('@babel/traverse');
traverse(ast, visitor);
複製代碼
visitor
是一個由各類type
或者是enter
和exit
組成的對象,由此肯定在遍歷的過程當中匹配到某種類型的節點後該如何操做,如咱們的需求是將Identifier
類型的log
節點替換爲MemberExpression
類型的console.log
,咱們能夠這樣定義visitor
:
const visitor = {
Identifier(path) {
const {node} = path;
if(node && node.name === 'log') {
path.replaceWith(createMemberExpression());
path.stop();
}
}
}
複製代碼
經過traverse
方法咱們能夠定義各類類型節點的操做方式,回調函數的path
參數提供了豐富的增、刪、改、查以及類型斷言的方法,好比replaceWith/remove/find/isMemberExpression
;
最後,咱們將修改後的AST
轉換爲Javascript
代碼:
const {code} = generate(ast, { /* options */ }, codes);
複製代碼
到這一步,咱們就已經能夠將代碼裏的log
方法替換爲console.log
了;觸類旁通,咱們是否是能夠放開一下想象力:本身定義某種有意思或者創造性的語法規範,而後經過AST
操做變換成常規的Javascript
;
以上例子的代碼彙總爲:
const t = require('@babel/types');
const {parse} = require('@babel/parser');
const {default: traverse} = require('@babel/traverse');
const {default: generate} = require('@babel/generator');
const codes = "log('Hello, world!');";
const ast = parse(codes, {
sourceType: "module"
});
const visitor = {
Identifier(path) {
const {node} = path;
if(node && node.name === 'log') {
path.replaceWith(createMemberExpression());
path.stop();
}
}
}
traverse(ast, visitor);
const {code} = generate(ast, { /* options */ }, codes);
console.log(code);
// console.log('Hello, world!');
function createMemberExpression() {
return t.memberExpression(
t.identifier('console'),
t.identifier('log')
);
}
複製代碼
以上, 經過babeljs
提供的相關模塊對AST
操做進行了初步的實踐; 2019 年已過一半,今年無疑跨端應用火了,好比,Taro、uni-app 等,而這些框架的成功通通離不開的就是對AST
的熟練掌握;因此呢,還等什麼,如今上車還來得及,若是你對前端報有天馬行空的想象,那麼我想了解AST
將會助你一臂之力!