手把手帶你入門AST抽象語法樹

NO IMAGE

AST 是什麼

抽象語法樹 (Abstract Syntax Tree),簡稱 AST,它是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。

AST 有什麼用

AST 運用廣泛,比如:

  • 編輯器的錯誤提示、代碼格式化、代碼高亮、代碼自動補全;
  • elintpretiier 對代碼錯誤或風格的檢查;
  • webpack 通過 babel 轉譯 javascript 語法;

並且如果你想了解 js 編譯執行的原理,那麼你就得了解 AST。

AST 如何生成

js 執行的第一步是讀取 js 文件中的字符流,然後通過詞法分析生成 token,之後再通過語法分析( Parser )生成 AST,最後生成機器碼執行。

整個解析過程主要分為以下兩個步驟:

  • 分詞:將整個代碼字符串分割成最小語法單元數組
  • 語法分析:在分詞基礎上建立分析語法單元之間的關係

JS Parser 是 js 語法解析器,它可以將 js 源碼轉成 AST,常見的 Parser 有 esprima、traceur、acorn、shift 等。

詞法分析

詞法分析,也稱之為掃描(scanner),簡單來說就是調用 next() 方法,一個一個字母的來讀取字符,然後與定義好的 JavaScript 關鍵字符做比較,生成對應的Token。Token 是一個不可分割的最小單元:

例如 var 這三個字符,它只能作為一個整體,語義上不能再被分解,因此它是一個 Token。

詞法分析器裡,每個關鍵字是一個 Token ,每個標識符是一個 Token,每個操作符是一個 Token,每個標點符號也都是一個 Token。除此之外,還會過濾掉源程序中的註釋和空白字符(換行符、空格、製表符等。

最終,整個代碼將被分割進一個tokens列表(或者說一維數組)。

語法分析

語法分析會將詞法分析出來的 Token 轉化成有語法含義的抽象語法樹結構。同時,驗證語法,語法如果有錯的話,拋出語法錯誤。

說了這麼多我們來看下 javaScript 代碼片段轉成 AST 之後是什麼樣的我們拿一行簡單的代碼來展示

🌰例子 1

const fn = a => a;

手把手帶你入門AST抽象語法樹

如圖從這個 AST 語法樹我們就能夠很清楚的看出一個代碼他的具體含義,並且使用的是什麼語法,方法等。

用人話翻譯這個圖就是:用類型 const 聲明變量 fn 指向一個箭頭函數表達式,它的參數是 a 函數體也是 a。

🌰例子 2

const fn = a => {
let i = 1;
return a + i;
};

我們來看 body 這塊:

手把手帶你入門AST抽象語法樹

🌰例子 3

函數調用

function test(){
let a = 1;
console.log(a)
}

主要看 MemberExpression

手把手帶你入門AST抽象語法樹

以上截圖均是使用 Acorn 解析。使用 Acorn 的原因是據我瞭解在 parser 解析中,Acorn 是公認的最快的。並且我們使用的 Webpack 打包工具中 babel 用的也是 Acorn。

上述截圖的屬性是 AST 的一部分,這個結構包含了很多屬性。

  • VariableDeclaration 變量聲明
  • VariableDeclarator 變量聲明的描述
  • Expression 表達式節點

更多屬性展示:

  1. 可以去 AST explorer 可以在線看到不同的 parser 解析 js 代碼後得到的 AST。
  2. github 上看所有的 ESTree ESTree
  3. 關於屬性介紹的文檔 抽象語法樹AST介紹

實戰 AST 的運用

題目

通過上面介紹的 console.log AST,下面我們就來完成一個在調用 console.log(xx) 時候給前面加一個函數名,這樣用戶在打印時候能改方便看到是哪個函數調用的。

舉例

// 源代碼
function getData() {
console.log("data")
}
// --------------------
// 轉化後代碼
function getData() {
console.log("getData", "data");
}

介紹

首先介紹下我們需要使用的工具 Babel

  • @babel/parser : 將 js 代碼 ——->>> AST 抽象語法樹;
  • @babel/traverseAST 節點進行遞歸遍歷;
  • @babel/types 對具體的 AST 節點進行進行修改;
  • @babel/generator : AST 抽象語法樹 ——->>> 新的 js 代碼;

為什麼使用 babel ? 主要是比較好用(只對這個比較熟悉😭)。

進入 @babel/parser 官網開頭就介紹了它是使用的 Acorn 來解析 js 代碼成 AST 語法樹(說明確實 Acorn 比較好)。

手把手帶你入門AST抽象語法樹

開始碼起來

  1. 新建文件打開控制檯安裝需要的包
cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D
  1. 創建 js 文件, 編寫大致佈局如下 使用 AST
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
function compile(code) {
// 1.parse 將代碼解析為抽象語法樹(AST)
const ast = parser.parse(code);
// 2,traverse 轉換代碼
traverse.default(ast, {});
// 3. generator 將 AST 轉回成代碼
return generator.default(ast, {}, code);
}
const code = `
function getData() {
console.log("data")
}
`;
const newCode = compile(code)

使用 node 跑出結果,因為什麼都沒處理,輸出的是原代碼,

手把手帶你入門AST抽象語法樹

完善 compile 方法

function compile(code) {
// 1.parse
const ast = parser.parse(code);
// 2,traverse
const visitor = {
CallExpression(path) {
// 拿到 callee 數據
const { callee } = path.node;
// 判斷是否是調用了 console.log 方法
// 1. 判斷是否是成員表達式節點,上面截圖有詳細介紹
// 2. 判斷是否是 console 對象
// 3. 判斷對象的屬性是否是 log
const isConsoleLog =
types.isMemberExpression(callee) &&
callee.object.name === "console" &&
callee.property.name === "log";
if (isConsoleLog) {
// 如果是 console.log 的調用 找到上一個父節點是函數
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
});
// 取函數的名稱
const funcName = funcPath.node.id.name;
// 將名稱通過 types 來放到函數的參數前面去
path.node.arguments.unshift(types.stringLiteral(funcName));
}
}
};
// traverse 轉換代碼
traverse.default(ast, visitor);
// 3. generator 將 AST 轉回成代碼
return generator.default(ast, {}, code);
}

純代碼看起來比較難理解下面是我將上面的 path.node 寫入到文件中給大家看下數據格式。

{
"type": "CallExpression",
"start": 24,
"end": 43,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 21 }
},
"callee": {
"type": "MemberExpression",
"start": 24,
"end": 35,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 13 }
},
"object": {
"type": "Identifier",
"start": 24,
"end": 31,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 9 },
"identifierName": "console"
},
"name": "console"
},
"property": {
"type": "Identifier",
"start": 32,
"end": 35,
"loc": {
"start": { "line": 3, "column": 10 },
"end": { "line": 3, "column": 13 },
"identifierName": "log"
},
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "StringLiteral",
"start": 36,
"end": 42,
"loc": {
"start": { "line": 3, "column": 14 },
"end": { "line": 3, "column": 20 }
},
"extra": { "rawValue": "data", "raw": "'data'" },
"value": "data"
}
]
}

我們將不必要的位置信息(start, end, loc)屬性刪除,對照數據來看代碼將會一目瞭然

手把手帶你入門AST抽象語法樹

再跑該文件

手把手帶你入門AST抽象語法樹

很好,調用 console.log 方法參數前面增加了函數名,完成!!

為了大家能夠方便運行,下面是完整代碼

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");
function compile(code) {
// 1.parse
const ast = parser.parse(code);
// 2,traverse
const visitor = {
CallExpression(path) {
const { callee } = path.node;
const isConsoleLog =
types.isMemberExpression(callee) &&
callee.object.name === "console" &&
callee.property.name === "log";
if (isConsoleLog) {
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
});
const funcName = funcPath.node.id.name;
fs.writeFileSync("./funcPath.json", JSON.stringify(funcPath.node), err => {
if (err) throw err;
console.log("寫入成功");
});
path.node.arguments.unshift(types.stringLiteral(funcName));
}
}
};
traverse.default(ast, visitor);
// 3. generator
return generator.default(ast, {}, code);
}
const code = `
function getData() {
console.log('data')
}
`;
console.log(compile(code).code);

看到這裡,如果你覺得都沒什麼問題,相信你對 AST 已經有了很清楚的認識了,並且對 babel 編譯代碼也有了一定的理解,以後寫 webpack 配置也就不會對 babel 那麼陌生了。

總結

為了兼容低版本瀏覽器 我們也通常會使用 webpack 打包編譯我們的代碼將 ES6 語法降低版本,比如箭頭函數變成普通函數。將 const、let 聲明改成 var 等等,他都是通過 AST 來完成的,只不過實現的過程比較複雜,精緻。不過也都是這三板斧:

  1. js 語法解析成 AST;
  2. 修改 AST;
  3. AST 轉成 js 語法;

最後

有時間,大家在嘗試完成之後也同樣可以試試箭頭函數轉普通函數等一些常用的代碼轉換,這樣可以很好的加深印象。

全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

參考

相關文章

前端性能優化圖片懶加載(防抖、節流)

告訴你如何關閉騰訊廣告定向投放

一位18屆前端玩家的年終總結|年度徵文

記錄Computed源碼分析