動態

詳情 返回 返回

從Babel開始認識AST抽象語法樹 - 動態 詳情

前言

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 表達式

公共屬性

類型 説明
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長什麼樣
    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以及插件的用法。

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

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

user avatar linx 頭像 hea1066 頭像 tinygeeker 頭像 alienzhou 頭像 coderleo 頭像 zhaoxiaoman 頭像 nut 頭像 jinjidedacong 頭像 yanglinxiao 頭像 aaaaaajie 頭像 qiangjiandebinggan 頭像 huagntm 頭像
點贊 24 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.