從Babel開始認識AST抽象語法樹

南玖發表於2023-01-10

前言

AST抽象語法樹想必大家都有聽過這個概念,但是不是隻停留在聽過這個層面呢。其實它對於程式設計來講是一個非常重要的概念,當然也包括前端,在很多地方都能看見AST抽象語法樹的影子,其中不乏有vue、react、babel、webpack、typeScript、eslint等。簡單來說但凡需要編譯的地方你基本都能發現AST的存在。

babel是用來將javascript高階語法編譯成瀏覽器能夠執行的語法,我們可以從babel出發來了解AST抽象語法樹。

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新文章~

babel編譯流程

瞭解AST抽象語法樹之前我們先來簡單瞭解一下babel的編譯流程,以及AST在babel編譯過程中起到了什麼作用?

1-babel-ast.png

我這裡畫了張圖方便理解babel編譯的整個流程

  • parse: 用於將原始碼編譯成AST抽象語法樹
  • transform: 用於對AST抽象語法樹進行改造
  • generator: 用於將改造後的AST抽象語法樹轉換成目的碼

很明顯AST抽象語法樹在這裡充當了一箇中間人的身份,作用就是可以透過對AST的操作還達到原始碼到目的碼的轉換過程,這將會比暴力使用正則匹配要優雅的多。

AST抽象語法樹

在電腦科學中,抽象語法樹(Abstract Syntax Tree,AST) 是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。

雖然在日常業務中我們可能很少會涉及到AST層面,但如果你想在babelwebpack等前端工程化上有所深度,AST將是你深入的基礎。

預覽AST

說了這麼多,那麼AST到底長什麼樣呢?

接下來我們可以透過工具AST Explorer來直觀的感受一下!

比如我們如下程式碼:

let fn = () => {
  console.log('前端南玖')
}

它最終生成的AST是這樣的:

2-babel-ast.png

  • 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表示式

公共屬性

型別說明
typeAST 節點的型別
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語法樹來轉換成自己想要生成的目的碼。

常見的解析器

第一個用JavaScript編寫的符合EsTree規範的JavaScript的解析器,後續多個編譯器都是受它的影響

一個小巧、快速的 JavaScript 解析器,完全用 JavaScript 編寫

babel官方的解析器,最初fork於acorn,後來完全走向了自己的道路,從babylon改名之後,其構建的外掛體系非常強大

UglifyJS 是一個 JavaScript 解析器、縮小器、壓縮器和美化器工具包。

esbuild是用go編寫的下一代web打包工具,它擁有目前最快的打包記錄和壓縮記錄,snowpack和vite的也是使用它來做打包工具,為了追求卓越的效能,目前沒有將AST進行暴露,也無法修改AST,無法用作解析對應的JavaScript。

AST應用

瞭解完AST,你會發現我們可以用它做許多複雜的事情,我們先來利用@babel/core簡單實現一個移除console的外掛來感受一下吧。

這個其實就是找規律,你只要知道console語句在AST上是怎樣表現的就能夠透過這一特點精確找到所有的console語句並將其移出就好了。

  • 先來看下console語句的AST長什麼樣
    3-babel-ast.png

很明顯它是一個表示式節點,所以我們只需要找到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)

4-babel-ast.png

從執行結果來看,它找到了兩個console語句,並且都將它們移除了

這就是對AST的簡單應用,學會AST能做的遠不止這些像前端大部分比較高階的內容都能看到它的存在。後面會繼續更新Babel以及外掛的用法。

原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」,如果你想進前端交流群一起學習,請點這裡

我是南玖,我們下期見!!!

相關文章