前言
AST抽象語法樹想必大家都有聽過這個概念,但是不是隻停留在聽過這個層面呢。其實它對於編程來講是一個非常重要的概念,當然也包括前端,在很多地方都能看見AST抽象語法樹的影子,其中不乏有vue、react、babel、webpack、typeScript、eslint等。簡單來説但凡需要編譯的地方你基本都能發現AST的存在。
babel是用來將javascript高級語法編譯成瀏覽器能夠執行的語法,我們可以從babel出發來了解AST抽象語法樹。
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新文章~
babel編譯流程
瞭解AST抽象語法樹之前我們先來簡單瞭解一下babel的編譯流程,以及AST在babel編譯過程中起到了什麼作用?
我這裏畫了張圖方便理解babel編譯的整個流程
- parse: 用於將源代碼編譯成AST抽象語法樹
- transform: 用於對AST抽象語法樹進行改造
- generator: 用於將改造後的AST抽象語法樹轉換成目標代碼
很明顯AST抽象語法樹在這裏充當了一箇中間人的身份,作用就是可以通過對AST的操作還達到源代碼到目標代碼的轉換過程,這將會比暴力使用正則匹配要優雅的多。
AST抽象語法樹
在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST) 是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。
雖然在日常業務中我們可能很少會涉及到AST層面,但如果你想在babel、webpack等前端工程化上有所深度,AST將是你深入的基礎。
預覽AST
説了這麼多,那麼AST到底長什麼樣呢?
接下來我們可以通過工具AST Explorer來直觀的感受一下!
比如我們如下代碼:
let fn = () => {
console.log('前端南玖')
}
它最終生成的AST是這樣的:
- AST抽象語法樹是源代碼語法結構的一種抽象表示
- 每個包含type屬性的數據結構,都是一個AST節點
- 它以樹狀的形式表現編程語言的語法結構,每個節點都表示源代碼中的一種結構
AST結構
為了統一ECMAScript標準的語法表達。社區中衍生出了ESTree Spec,是目前前端所遵循的一種語法表達標準。
節點類型
| 類型 | 説明 |
|---|---|
| File | 文件 (頂層節點包含 Program) |
| Program | 整個程序節點 (包含 body 屬性代表程序體) |
| Directive | 指令 (例如 "use strict") |
| Comment | 代碼註釋 |
| Statement | 語句 (可獨立執行的語句) |
| Literal | 字面量 (基本數據類型、複雜數據類型等值類型) |
| Identifier | 標識符 (變量名、屬性名、函數名、參數名等) |
| Declaration | 聲明 (變量聲明、函數聲明、Import、Export 聲明等) |
| Specifier | 關鍵字 (ImportSpecifier、ImportDefaultSpecifier、ImportNamespaceSpecifier、ExportSpecifier) |
| Expression | 表達式 |
公共屬性
| 類型 | 説明 |
|---|---|
| type | AST 節點的類型 |
| start | 記錄該節點代碼字符串起始下標 |
| end | 記錄該節點代碼字符串結束下標 |
| loc | 內含 line、column 屬性,分別記錄開始結束的行列號 |
| leadingComments | 開始的註釋 |
| innerComments | 中間的註釋 |
| trailingComments | 結尾的註釋 |
| extra | 額外信息 |
AST是如何生成的
一般來講生成AST抽象語法樹都需要javaScript解析器來完成
JavaScript解析器通常可以包含四個組成部分:
- 詞法分析器(Lexical Analyser)
- 語法解析器(Syntax Parser)
- 字節碼生成器(Bytecode generator)
- 字節碼解釋器(Bytecode interpreter)
詞法分析
這裏主要是對代碼字符串進行掃描,然後與定義好的 JavaScript 關鍵字符做比較,生成對應的Token。Token 是一個不可分割的最小單元。
詞法分析器裏,每個關鍵字是一個 Token ,每個標識符是一個 Token,每個操作符是一個 Token,每個標點符號也都是一個 Token,詞法分析過程中不會關心單詞與單詞之間的關係.
除此之外,還會過濾掉源程序中的註釋和空白字符、換行符、空格、製表符等。最終,整個代碼將被分割進一個tokens列表
javaScript中常見的token主要有:
關鍵字:var、let、const等
標識符:沒有被引號括起來的連續字符,可能是一個變量,也可能是 if、else 這些關鍵字,又或者是 true、false 這些內置常量
運算符: +、-、 *、/ 等
數字:像十六進制,十進制,八進制以及科學表達式等
字符串:變量的值等
空格:連續的空格,換行,縮進等
註釋:行註釋或塊註釋都是一個不可拆分的最小語法單元
標點:大括號、小括號、分號、冒號等
比如我們還是這段代碼:
let fn = () => {
console.log('前端南玖')
}
它在經過詞法分析後生成的token是這樣的:
工具:esprima
[
{
"type": "Keyword",
"value": "let"
},
{
"type": "Identifier",
"value": "fn"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Punctuator",
"value": "("
},
{
"type": "Punctuator",
"value": ")"
},
{
"type": "Punctuator",
"value": "=>"
},
{
"type": "Punctuator",
"value": "{"
},
{
"type": "Identifier",
"value": "console"
},
{
"type": "Punctuator",
"value": "."
},
{
"type": "Identifier",
"value": "log"
},
{
"type": "Punctuator",
"value": "("
},
{
"type": "String",
"value": "'前端南玖'"
},
{
"type": "Punctuator",
"value": ")"
},
{
"type": "Punctuator",
"value": "}"
}
]
拆分出來的每個字符都是一個token
語法分析
這個過程也稱為解析,是將詞法分析產生的token按照某種給定的形式文法轉換成AST的過程。也就是把單詞組合成句子的過程。在轉換過程中會驗證語法,語法如果有錯的話,會拋出語法錯誤。
還是上面那段代碼,在經過語法分析後生成的AST是這樣的:
工具:AST Explorer
{
"type": "VariableDeclaration", // 節點類型: 變量聲明
"declarations": [ // 聲明
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier", // 標識符
"name": "fn" // 變量名
},
"init": {
"type": "ArrowFunctionExpression", // 箭頭函數表達式
"id": null,
"generator": false,
"async": false,
"params": [], // 函數參數
"body": { // 函數體
"type": "BlockStatement", // 語句塊
"body": [
{
"type": "ExpressionStatement", // 表達式語句
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"identifierName": "console"
},
"name": "console"
},
"computed": false,
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [ // 函數參數
{
"type": "StringLiteral", // 字符串
"extra": {
"rawValue": "前端南玖",
"raw": "'前端南玖'"
},
"value": "前端南玖"
}
]
}
],
"directives": []
}
}
}
],
"kind": "let" // 變量聲明類型
}
在得到AST抽象語法樹之後,我們就可以通過改造AST語法樹來轉換成自己想要生成的目標代碼。
常見的解析器
- Esprima
第一個用JavaScript編寫的符合EsTree規範的JavaScript的解析器,後續多個編譯器都是受它的影響
- acorn
一個小巧、快速的 JavaScript 解析器,完全用 JavaScript 編寫
- @babel/parser(Babylon)
babel官方的解析器,最初fork於acorn,後來完全走向了自己的道路,從babylon改名之後,其構建的插件體系非常強大
- UglifyJS
UglifyJS 是一個 JavaScript 解析器、縮小器、壓縮器和美化器工具包。
- esbuild
esbuild是用go編寫的下一代web打包工具,它擁有目前最快的打包記錄和壓縮記錄,snowpack和vite的也是使用它來做打包工具,為了追求卓越的性能,目前沒有將AST進行暴露,也無法修改AST,無法用作解析對應的JavaScript。
AST應用
瞭解完AST,你會發現我們可以用它做許多複雜的事情,我們先來利用@babel/core簡單實現一個移除console的插件來感受一下吧。
這個其實就是找規律,你只要知道console語句在AST上是怎樣表現的就能夠通過這一特點精確找到所有的console語句並將其移出就好了。
- 先來看下console語句的AST長什麼樣
很明顯它是一個表達式節點,所以我們只需要找到name為console的表達式節點刪除即可。
- 編寫plugin
const babel = require("@babel/core")
let originCode = `
let fn = () => {
const a = 1
console.log('前端南玖')
if(a) {
console.log(a)
}else {
return false
}
}
`
let removeConsolePlugin = function() {
return {
// 訪問器
visitor: {
CallExpression(path, state) {
const { node } = path
if(node?.callee?.object?.name === 'console') {
console.log('找到了console語句')
path.parentPath.remove()
}
}
}
}
}
const options = {
plugins: [removeConsolePlugin()]
}
let res = babel.transformSync(originCode, options)
console.dir(res.code)
從執行結果來看,它找到了兩個console語句,並且都將它們移除了
這就是對AST的簡單應用,學會AST能做的遠不止這些像前端大部分比較高級的內容都能看到它的存在。後面會繼續更新Babel以及插件的用法。
原文首發地址點這裏,歡迎大家關注公眾號 「前端南玖」,如果你想進前端交流羣一起學習,請點這裏
我是南玖,我們下期見!!!