作者:東北烤冷麵@毛豆前端
一、什麼是AST
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。
二、AST有什麼作用
抽象語法樹在很多領域有廣泛的應用,比如瀏覽器,智慧編輯器,編譯器等。在JavaScript中,雖然我們並不會常常與AST直接打交道,但卻也會經常的涉及到它。例如使用UglifyJS來壓縮程式碼,bable對程式碼進行轉換,ts型別檢查,語法高亮等,實際這背後就是在對JavaScript的抽象語法樹進行操作。
三、AST生成過程
javascript的抽象語法樹的生成主要依靠的是Javascript Parser(js解析器),整個解析過程分為兩個階段:
1.詞法分析(Lexical Analysis)
詞法分析是電腦科學中將字元序列轉換為單詞(Token)序列的過程,進行詞法分析的程式叫做詞法分析器,也叫掃描器(Scanner)。
//code
let age='18'
//tokens
[
{
value: 'let',
type:'identifier'
},
{
type:'whitespace',
value:' '
},
{
value: 'age',
type:'identifier'
},
{
value: '=',
type:'operator'
},
{
value: '=',
type:'operator'
},
{
value: '18',
type:'num'
},
]
複製程式碼
2.語法分析(Parse Analysis)
語法分析是編譯過程的一個邏輯階段。語法分析的任務是在詞法分析的基礎上將單詞序列組合成語法樹,如“程式”,“語句”,“表示式”等等.語法分析程式判斷源程式在結構上是否正確。源程式的結構由上下文無關文法描述。
{
"type": "Program",
"start": 0,
"end": 12,
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "age"
},
"init": {
"type": "Literal",
"value": "18",
"raw": "'18'"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
複製程式碼
常見的Javascript Parser有很多:
- babylon:應用於bable
- acorn:應用於webpack
- espree:應用於eslint
四、拿babel為例
Babel是一個常用的工具,它的工作過程經過三個階段,解析(parsing)、轉換(transform)、生成(generate),如下圖所示,在parse階段,babel使用babylon庫將原始碼轉換為AST,在transform階段,利用各種外掛進行程式碼轉換,在generator階段,再利用程式碼生成工具,將AST轉換成程式碼。
一個簡單的需求進行說明
我們想在程式碼中的console列印出來的內容前面加上它所在的函式名稱,程式碼如下:
// index.js
function compile(code) {
// todo
}
const code = `
function foo(){
console.log('bar')
}
`
const result = compile(code)
console.log(result.code)
複製程式碼
首先我們先安裝bable的全家桶工具:
yarn add @babel/{parser,traverse,types,generator}
複製程式碼
然後將其引入檔案中:
const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
//tode
}
const code = `
function foo(){
console.log('bar')
}
`
const result = compile(code)
console.log(result.code)
複製程式碼
我們可以通過AST Explorer檢視code程式碼的抽象語法樹結構,注意,這裡面我們的解析工具要選用babylon7,這樣和我們例子中程式碼解析出的結構才匹配
先解析拿到AST,直接生成程式碼片段:
const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
// 1. 解析
const ast = parser.parse(code)
// 2. 遍歷
// 3. 生成程式碼片段
return generator.default(ast, {}, code)
}
const code = `
function foo(){
console.log('bar')
}
`
const result = compile(code)
console.log(result.code)
複製程式碼
執行一下
node index.js
複製程式碼
輸出結果
說明我們的程式碼沒有問題,已經跑通了!剩下的只需要我們在第二階段進行處理了。
第二階段
需要使用到訪問者(Visitors),訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個物件,定義了用於在一個樹狀結構中獲取具體節點的方法。這麼說有些抽象所以讓我們來看一個例子。
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
// 你也可以先建立一個訪問者物件,並在稍後給它新增方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
複製程式碼
這是一個簡單的訪問者,把它用於遍歷中時,每當在樹中遇見一個 Identifier
的時候會呼叫 Identifier()
方法。
所以在下面的程式碼中 Identifier()
方法會被呼叫四次(包括 square
在內,總共有四個 Identifier
)。).
function square(n) {
return n * n;
}
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!
複製程式碼
回到我們的例子,我們只需要建立一個訪問者,訪問到CallExpression節點,然後通過判斷,去修改它arguments屬性的引數就可以完成我們的任務了
修改我們的程式碼
const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
// 1. 解析
const ast = parser.parse(code)
// 2. 遍歷
//visitor可以對特定節點進行處理
const visitor = {
//定義需要轉換的節點CallExpression
CallExpression(path) {
//獲取當前的節點
const { callee } = path.node;
//判斷
if (
t.isMemberExpression(callee)
&&
callee.object.name === 'console'
&&
callee.property.name === 'log'
) {
// 獲取上層FunctionDeclaration路徑
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
})
// 將上層函式名新增到引數前
path.node.arguments.unshift(
t.stringLiteral(`function name ${funcPath.node.id.name}:`)
)
}
}
}
traverse.default(ast, visitor)
// 3. 生成程式碼片段
return generator.default(ast, {}, code)
}
const code = `
function foo(){
console.log('bar')
}
`
const result = compile(code)
console.log(result.code)
複製程式碼
我們再來列印下
這樣我們就完成了整個任務,當然這只是一個很簡單的例子,在實際開發中,我們還需要進行更復雜的判斷才能保證我們的功能完善。