做者:東北烤冷麪@毛豆前端javascript
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。前端
抽象語法樹在不少領域有普遍的應用,好比瀏覽器,智能編輯器,編譯器等。在JavaScript中,雖然咱們並不會經常與AST直接打交道,但卻也會常常的涉及到它。例如使用UglifyJS來壓縮代碼,bable對代碼進行轉換,ts類型檢查,語法高亮等,實際這背後就是在對JavaScript的抽象語法樹進行操做。java
javascript的抽象語法樹的生成主要依靠的是Javascript Parser(js解析器),整個解析過程分爲兩個階段: node
詞法分析是計算機科學中將字符序列轉換爲單詞(Token)序列的過程,進行詞法分析的程序叫作詞法分析器,也叫掃描器(Scanner)。webpack
//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'
},
]
複製代碼
語法分析是編譯過程的一個邏輯階段。語法分析的任務是在詞法分析的基礎上將單詞序列組合成語法樹,如「程序」,「語句」,「表達式」等等.語法分析程序判斷源程序在結構上是否正確。源程序的結構由上下文無關文法描述。git
{
"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有不少:github
Babel是一個經常使用的工具,它的工做過程通過三個階段,解析(parsing)、轉換(transform)、生成(generate),以下圖所示,在parse階段,babel使用babylon庫將源代碼轉換爲AST,在transform階段,利用各類插件進行代碼轉換,在generator階段,再利用代碼生成工具,將AST轉換成代碼。
web
咱們想在代碼中的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)
複製代碼
咱們再來打印下
這樣咱們就完成了整個任務,固然這只是一個很簡單的例子,在實際開發中,咱們還須要進行更復雜的判斷才能保證咱們的功能完善。