首頁 > 科技

你怎麼可以不瞭解 AST 呢?

2021-07-15 03:17:58

周世鐵,微醫前端技術部醫療支撐組前端亂構師

前言

在我們編寫業務程式碼的時候,可能很少人會使用到AST,以至於大多數同學都不大瞭解AST。有的同學曾經學過,但是不去實踐的話,過段時間又忘的差不多了。看到這裡,你會發現說的就是你。聽說貴圈現在寫文章都要編故事,時不時還要整點表情包。這是真的嗎?作為公司最頭鐵的前端,我就不放。

本文將通過以下幾個方面對AST進行學習

  1. 基礎知識

  • AST是什麼
  • AST有什麼用
  • AST如何生成

  • 實戰小例子

    • 去掉debugger
    • 修改函數中執行的console.log參數

  • 總結

  • 基礎知識

    AST是什麼

    先貼下官方的解釋

    在電腦科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。

    為了方便大家理解抽象語法樹,來看看具體的例子。

    var tree = 'this is tree'
    js 源程式碼將會被轉化成下面的抽象語法樹

    {  "type": "Program",  "start": 0,  "end": 25,  "body": [    {      "type": "VariableDeclaration",      "start": 0,      "end": 25,      "declarations": [        {          "type": "VariableDeclarator",          "start": 4,          "end": 25,          "id": {            "type": "Identifier",            "start": 4,            "end": 8,            "name": "tree"          },          "init": {            "type": "Literal",            "start": 11,            "end": 25,            "value": "this is tree",            "raw": "'this is tree'"          }        }      ],      "kind": "var"    }  ],  "sourceType": "module"}
    可以看到一條語句由若干個詞法單元組成。這個詞法單元就像 26 個字母。創造出個十幾萬的單詞,通過不同單詞的組合又能寫出不同內容的文章。

    至於有哪些詞法單元可點選檢視AST 物件文件 或者 參考掘金大佬的文章高階前端基礎-JavaScript 抽象語法樹 AST裡面列舉了語法樹節點與解釋。

    推薦一個工具 線上 ast 轉換器。可以在這個網站上,親自嘗試下轉換。點選語句中的詞,右邊的抽象語法樹節點便會被選中,如下圖:

    tree.jpg

    AST 有什麼用

    • IDE 的錯誤提示、程式碼格式化、程式碼高亮、程式碼自動補全等
    • JSLint、JSHint 對程式碼錯誤或風格的檢查等
    • webpack、rollup 進行程式碼打包等
    • CoffeeScript、TypeScript、JSX 等轉化為原生 Javascript.
    • vue 模板編譯、react 模板編譯

    AST 如何生成

    看到這裡,你應該已經知道抽象語法樹大致長什麼樣了。那麼AST又是如何生成的呢?

    AST 整個解析過程分為兩個步驟:

    • 詞法分析 (Lexical Analysis):掃描輸入的原始碼字元串,生成一系列的詞法單元 (tokens)。這些詞法單元包括數字,標點符號,運算符等。詞法單元之間都是獨立的,也即在該階段我們並不關心每一行程式碼是通過什麼方式組合在一起的。
    • 語法分析 (Syntax Analysis):建立分析語法單元之間的關係

    還是以上面var tree = 'this is tree'為例

    正規理解

    • 詞法分析

    先經過詞法分析,掃描輸入的原始碼字元串,生成一系列的詞法單元 (tokens)。這些詞法單元包括數字,標點符號,運算符等

    tokens.png

    • 語法分析

    語法分析階段就會將上一階段生成的 tokens 列表轉換為如下圖所示的 AST(我把start、end欄位去掉了不用在意)

    ast.png

    非正規理解

    鄭重聲明:我周某人語文很少及格,大致意思能理解就好。

    例子:"它是豬。"

    • 詞法分析

    先經過詞法分析,掃描輸入的原始碼字元串,生成一系列的詞法單元 (tokens)。這些詞法單元包括數字,標點符號,運算符等

    chinese_tokens.png

    • 語法分析

    語法分析階段就會將上一階段生成的 tokens 列表轉換為如下圖所示的 AST

    chinese_ast.png

    • JsParser

    JavaScript Parser,把 js 源碼轉化為抽象語法樹的解析器。

    • acorn
    • esprima
    • traceur
    • @babel/parser

    實戰小例子

    例子 1:去 debugger

    原始碼:

    function fn() {  console.log('debugger')  debugger;}
    根據前面學過的知識點,我們先腦海中意淫下如何去掉這個debugger

    1. 先將原始碼轉化成AST
    2. 遍歷**AST**上的節點,找到**debugger**節點,並刪除
    3. 將轉換過的AST再生成JS程式碼

    將原始碼拷貝到 線上 ast 轉換器 中,並點選左邊區域的debugger,可以看到左邊的debugger節點就被選中了。所以只要把圖中選中的debugger抽象語法樹節點刪除就行了。

    這個例子比較簡單,直接上程式碼。

    這個例子我使用@babel/parser、@babel/traverse、@babel/generator,它們的作用分別是解析、轉換、生成。

    const parser = require('@babel/parser');const traverse = require("@babel/traverse");const generator = require("@babel/generator");
    // 原始碼const code = `function fn() { console.log('debugger') debugger;}`;
    // 1. 原始碼解析成 astconst ast = parser.parse(code);

    // 2. 轉換const visitor = { // traverse 會遍歷樹節點,只要節點的 type 在 visitor 物件中出現,變化呼叫該方法 DebuggerStatement(path) { // 刪除該抽象語法樹節點 path.remove(); }}traverse.default(ast, visitor);
    // 3. 生成const result = generator.default(ast, {}, code);
    console.log(result.code)
    // 4. 日誌輸出
    // function fn() {// console.log('debugger');// }

    babel核心邏輯處理都在visitor裡。traverse會遍歷樹節點,只要節點的type在visitor物件中出現,便會呼叫該type對應的方法,在方法中呼叫path.remove()將當前節點刪除。demo中使用到的path的一些api可以參考babel-handbook。

    例子 2:修改函數中執行的 console.log 參數

    我們有時候在函數裡打了日誌,但是又想在控制檯直觀的看出是哪個函數中打的日誌,這個時候就可以使用AST,去解析、轉換、生成最後想要的程式碼。

    原始碼:

    function funA() { console.log(1)}// 轉換成
    function funA() { console.log('from function funA:', 1)}

    在編碼之前,我們先理清思路,再下手也不遲。這個時候就需要藉助 線上 ast 轉換器來分析了。

    funA.png

    通過工具發現想要實現這個案例只需要往arguments前面插入段節點就可以了。

    這裡也像例子 1 一樣先梳理下思路

    1. 使用 @babel/parser 將原始碼解析成 ast
    2. 監聽 @babel/traverse 遍歷到 CallExpression
    3. 觸發後,判斷如果執行的方法是 console.log 時,往 arguments unshift一個 StringLiteral
    4. 將轉換後的 ast 生成程式碼

    將 js 程式碼解析成 ast 與 將 ast 生成 js 程式碼與去 debugger 例子一致,這裡將不再描述。

    首先監聽CallExpression遍歷

    const visitor = {  CallExpression(path) {    // console.log(path)  }

    觀察 線上 ast 轉換器 解析後的樹,我們只要判斷path 的 callee中存在物件 console 及屬性 property 。就可以往當前的 path 的 arguments unshift 一個 StringLiteral

    這裡的 types 物件是使用了一個新包 @babel/types , 用來判斷類型。

    上面用到的isMemberExpression,isIdentifier,getFunctionParent,stringLiteral都可以在babel-handbook文件中找到,本文就不解釋了。

    const visitor = {  // 當遍歷到 CallExpression 時候觸發  CallExpression(path) {    const callee = path.node.callee;    // 判斷當前當前執行的函數是否是組合表示式    if (types.isMemberExpression(callee)) {      const { object, property } = callee;      if (types.isIdentifier(object, { name: 'console' }) && types.isIdentifier(property, { name: 'log' })) {        // 查詢最接近的父函數或程式        const parent = path.getFunctionParent();        const parentFunName = parent.node.id.name;        path.node.arguments.unshift(types.stringLiteral(`from function ${parentFunName}`))      }    }  }}

    總結

    就像前言所說的,我們的日常工作中很少會去使用AST,以至於大多數同學都不大瞭解AST。但瞭解了 AST 可以幫助我們更好地理解開發工具、編譯器的原理,併產出提高程式碼效率的工具。還記得我在之前的前端小組遇到一個問題,我們項目是ssr項目,在服務端執行的生命週期不允許出現客戶端才能執行的程式碼。但是小組成員有時候無意的寫了,導致服務端渲染降級。在學習AST之前,我為了解決這個問題,寫了個loader通過正則去匹配校驗,當時可真是逼死我了,正則需要去適配各種場景。後面我學習了AST了之後,編寫了個eslint插件實現了客戶端程式碼校驗。

    參考

    • babel-handbook(https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md)
    • 深入 Babel,這一篇就夠了(https://juejin.im/post/6844903746804137991)
    • 高階前端基礎-JavaScript 抽象語法樹 AST(https://juejin.cn/post/6844903798347939853#heading-12)


    IT145.com E-mail:sddin#qq.com