AST是什麼
AST(Abstract Syntax Tree),抽象語法樹, 是程序的抽象意義表示。意味着跟語言的具體文法無關,利用抽象語法樹可以進行程序的處理和轉換。例如更改函數名稱,語法轉換甚至不同語言間的轉換。
AST的一個示例:
AST的各種應用場景
- 編譯器(compilor)
- DOM
- JS引擎
- 構建工具
- webpack
- 依賴查詢
- treeshaking
- babel
- webpack
- JS框架(Angular,React)
- js選擇器引擎——Sizle(語法簡單,更着重詞法分析)
那麼如何獲取一個程序的AST呢?
以下面的一個mini編譯器爲例,我們說明一下基本流程。
一個mini編譯器
將lisp語言的寫法轉成C語言類似的風格。
'(add 2 (substract 4 3))' ==> 'add(2, substract(4,3))'
題外話: lisp語言誕生歷史很久了,現在的高級語言基本都在向lisp語言的風格靠近
詞法解析
詞法解析是將輸入的程序解析出一個個的詞法最小單元(token)。例如在下面這行代碼中:
var num = 1 + 2;
‘var’, ‘num’, ‘=’, ‘1’, ‘+’, ‘2’, ‘;’ 就是這段程序的tokens。因爲語句中的’var’, 'num’等是不可再進一步拆分的最小語義單元。如果把’var’再繼續拆下去,就會失去本身表達的語義(聲明一個變量)。
而詞法解析的過程就是解析輸入的程序,輸出一個token的數組。
如何做詞法解析?
以程序 (add 2 (substract 4 3))
爲例,它的tokens按照我們的理解因該是:
["(", "add", "2", "(", "substract", "4", "3", ")", ")"]
我們只以如何解析 'add’這類名稱爲示例:
// input爲輸入的程序字符串,
// char爲在遍歷input過程中,當前的單個字符
// current 爲遍歷的下標
if (/[a-z]/i.test(char)) {
let val = '';
while (/[a-z]/.test(char)) {
val += char;
char = input[current++];
}
return val;
}
那麼詞法解析的意義是什麼呢?
詞法解析的目的是解析出程序中的語義單元,爲下一步的語義分析做準備。這就好比我們平時與人溝通,對方說出一句話後,之所以我們能夠理解是因爲我們清楚這句話中每個字的意思以及這些字連在一起後表示的最終意思。詞法解析的目標就是將程序這段話拆成一個個的字。作爲程序解析的下一步————語義分析的 輸入。
語義分析
語義分析的產出結果就是我們講到的AST。語義分析的基礎是在前一步詞法解析的基礎上進行的。一門程序語言是有語法存在的,舉個例子,如果程序中出現一個單引號"’", 那麼我們如何知道它是一個字符串的開始還是結束呢?這就需要判斷它在程序中出現的位置。只要跟位置關聯上,我們需要判斷程序的上下文了。所以說,語義分析的過程本質上是跟程序語言本身的語法關聯上的。語義分析也就是根據語法解析程序的過程。
那麼在本例中,經過語義分析後,上一步輸出的array就會被轉成object。
這個主要以代碼爲主:
function parser(tokens) {
let current = 0;
function walk() {
let token = tokens[current];
if(token.type === 'Number'){
current++;
return {
type: 'NumberLiteral',
value: token.value
}
}
if(token.type === 'String'){
current++;
return {
type: 'StringLiteral',
value: token.value
}
}
if(token.type === 'parent' && token.value ==='('){
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: []
};
token = tokens[++current];
while(
(token.type !== 'parent') ||
(token.type === 'parent' && token.value !== ')')
){
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
let ast = {
type: 'Program',
body: []
}
while(current < tokens.length){
ast.body.push(walk());
}
return ast;
}
遍歷語法樹
我們得到了程序的語法樹,語法樹等同於程序。如果我們需要修改程序,那麼修改AST就可以了。修改AST首先需要我們對它進行遍歷traverse。
- 在遍歷過程中,當我們遍歷到某個節點Node時,我們將在該節點停留的過程分爲三個階段:進入當前節點,處於當前節點,離開當前節點。
- 語法樹是嵌套結構,可以使用遞歸的思路。對於下一級是array結構的,我們使用
traverserArray
方法。
function traverser(ast, visitor){
function traverserArray(array, parent){
array.forEach(child=>{
traverserNode(child, parent);
})
}
function traverserNode(node, parent){
let methods = visitor[node.type];
if(methods && methods.enter){
methods.enter(node, parent);
}
switch(node.type){
case 'Program':
traverserArray(node.body, node);
break;
case 'CallExpression':
traverserArray(node.params, node);
break;
case 'NumberLiteral':
case 'StringLiteral':
break;
default:
throw new TypeError(node.type);
}
if(methods && methods.exit){
methods.exit(node, parent);
}
}
traverserNode(ast, null);
};
語法樹轉換(transform)
function transform(ast){
let newAst = {
type: 'Program',
body: []
};
ast._context = newAst.body;
traverser(ast, {
NumberLiteral: {
enter(node, parent){
parent._context.push({
type: 'NumberLiteral',
value: node.value
});
}
},
StringLiteral: {
enter(node, parent){
parent._context.push({
type: 'StringLiteral',
value: node.value
})
}
},
CallExpression: {
enter(node, parent){
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: []
};
node._context = expression.arguments;
if(parent.type !== 'CallExpression'){
expression = {
type: 'ExpressionStatement',
expression: expression
};
}
parent._context.push(expression);
}
}
});
return newAst;
}
代碼生成(generator)
抽象語法樹本身不具有在實際環境中直接運行的能力,我們還需要將它轉換成程序代碼,這樣纔在實際環境具有價值。
function generator(node){
switch(node.type){
case 'Program':
return node.body.map(generator).join('\n');
case 'ExpressionStatement':
return (generator(node.expression) + ';');
case 'CallExpression':
return (
generator(node.callee) + '(' + node.arguments.map(generator).join(',') + ')'
);
case 'Identifier':
return node.name;
case 'NumberLiteral':
return node.value;
case 'StringLiteral':
return '"'+node.value + '"';
default:
throw new TypeError(node.type);
}
}
AST的一些應用實例
代碼壓縮
壓縮代碼時,如何正確更改變量名稱?
語法檢查
在IDE中,實時檢查語法錯誤
代碼轉換
ES6+代碼轉成ES5代碼
虛擬DOM和模板
Vue.js, React, 和Angular都涉及到了模板/虛擬DOM。在解析模板/虛擬DOM時,都需要先轉成AST。
依賴分析
webpack進行依賴分析時,如何精確獲取依賴程序中的模塊ID
treeshaking(搖樹優化)
剔除代碼中未被引用的函數、變量或者模塊
Sizzle(jQuery的選擇器引擎)
在根據selector查找DOM時,需要解析selector爲一個個最小的token。例如$(.cls span)
會被解析成 ['.cls', 'span']
。
高級程序編譯
Java、C等高級語言轉成二進制語言的過程,更是避免不了轉成AST的步驟。
參考文章:
the-super-tiny-compiler
he SpiderMonkey parser API
關於ES語言抽象語法樹規範
Babel是如何讀懂JS代碼的
AST in Modern JavaScript