AST抽象語法樹想必大家都有聽過這個概念,但是不是隻停留在聽過這個層面呢。其實它對於程式設計來講是一個非常重要的概念,當然也包括前端,在很多地方都能看見AST抽象語法樹的影子,其中不乏有vue、react、babel、webpack、typeScript、eslint等。簡單來說但凡需要編譯的地方你基本都能發現AST的存在。
babel
是用來將javascript
高階語法編譯成瀏覽器能夠執行的語法,我們可以從babel
出發來了解AST抽象語法樹。
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖
第一時間獲取最新文章~
瞭解AST抽象語法樹之前我們先來簡單瞭解一下babel
的編譯流程,以及AST在babel
編譯過程中起到了什麼作用?
我這裡畫了張圖方便理解babel
編譯的整個流程
很明顯AST抽象語法樹
在這裡充當了一箇中間人的身份,作用就是可以通過對AST的操作還達到原始碼到目的碼的轉換過程,這將會比暴力使用正則匹配要優雅的多。
在電腦科學中,抽象語法樹(Abstract Syntax Tree,AST) 是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。
雖然在日常業務中我們可能很少會涉及到AST層面,但如果你想在babel
、webpack
等前端工程化上有所深度,AST將是你深入的基礎。
說了這麼多,那麼AST到底長什麼樣呢?
接下來我們可以通過工具AST Explorer來直觀的感受一下!
比如我們如下程式碼:
let fn = () => {
console.log('前端南玖')
}
它最終生成的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抽象語法樹
都需要javaScript解析器來完成
JavaScript解析器通常可以包含四個組成部分:
這裡主要是對程式碼字串進行掃描,然後與定義好的 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語法樹來轉換成自己想要生成的目的碼。
第一個用JavaScript編寫的符合EsTree規範的JavaScript的解析器,後續多個編譯器都是受它的影響
一個小巧、快速的 JavaScript 解析器,完全用 JavaScript 編寫
babel官方的解析器,最初fork於acorn,後來完全走向了自己的道路,從babylon改名之後,其構建的外掛體系非常強大
UglifyJS 是一個 JavaScript 解析器、縮小器、壓縮器和美化器工具包。
esbuild是用go編寫的下一代web打包工具,它擁有目前最快的打包記錄和壓縮記錄,snowpack和vite的也是使用它來做打包工具,為了追求卓越的效能,目前沒有將AST進行暴露,也無法修改AST,無法用作解析對應的JavaScript。
瞭解完AST,你會發現我們可以用它做許多複雜的事情,我們先來利用@babel/core
簡單實現一個移除console的外掛來感受一下吧。
這個其實就是找規律,你只要知道console語句在AST上是怎樣表現的就能夠通過這一特點精確找到所有的console語句並將其移出就好了。
很明顯它是一個表示式節點,所以我們只需要找到name為console的表示式節點刪除即可。
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以及外掛的用法。
我是南玖,我們下期見!!!
-------------------------------------------
個性簽名:智者創造機會,強者把握機會,弱者坐等機會。做一個靈魂有趣的人!
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~
歡迎加入前端技術交流群:928029210(QQ)
掃描下方二維條碼關注公眾號,回覆進群,拉你進前端學習交流群(WX),這裡有一群志同道合的前端小夥伴,交流技術、生活、內推、面經、摸魚,這裡都有哈,快來加入我們吧~ 回覆資料,獲取前端大量精選前端電子書及學習視訊~