聊一聊 Javascript 中的 AST

什么是抽象語法樹(Abstract Syntax Tree ,AST)?

百度百科是這么解釋的:

在計算機(jī)科學(xué)中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。

聽起來還是很繞,沒關(guān)系,你可以簡單理解為 它就是你所寫代碼的的樹狀結(jié)構(gòu)化表現(xiàn)形式

有了這棵樹,我們就可以通過操縱這顆樹,精準(zhǔn)的定位到聲明語句、賦值語句、運(yùn)算語句等等,實(shí)現(xiàn)對代碼的分析、優(yōu)化、變更等操作。

AST在日常業(yè)務(wù)中也許很難涉及到,有可能你還沒有聽過,但其實(shí)很多時候你已經(jīng)在使用它了,只是沒有太多關(guān)注而已,現(xiàn)在流行的 webpackeslint 等很多插件或者包都有涉及~

抽象語法樹能做什么?

聊到AST的用途,其應(yīng)用非常廣泛,下面我簡單羅列了一些:

  • IDE的錯誤提示、代碼格式化、代碼高亮、代碼自動補(bǔ)全等
  • JSLint、JSHint對代碼錯誤或風(fēng)格的檢查等
  • webpackrollup進(jìn)行代碼打包等
  • CoffeeScript、TypeScript、JSX等轉(zhuǎn)化為原生Javascript

其實(shí)它的用途,還不止這些,如果說你已經(jīng)不滿足于實(shí)現(xiàn)枯燥的業(yè)務(wù)功能,想寫出類似react、vue這樣的牛逼框架,或者想自己搞一套類似webpack、rollup這樣的前端自動化打包工具,那你就必須弄懂AST。

如何生成AST?

在了解如何生成AST之前,有必要了解一下Parser(常見的Parseresprima、traceur、acornshift等)

JS Parser其實(shí)是一個解析器,它是將js源碼轉(zhuǎn)化為抽象語法樹(AST)的解析器。

整個解析過程主要分為以下兩個步驟:

  • 分詞:將整個代碼字符串分割成最小語法單元數(shù)組
  • 語法分析:在分詞基礎(chǔ)上建立分析語法單元之間的關(guān)系

什么是語法單元?

語法單元是被解析語法當(dāng)中具備實(shí)際意義的最小單元,簡單的來理解就是自然語言中的詞語。

舉個例子來說,下面這段話:

“2019年是祖國70周年”

我們可以把這句話拆分成最小單元,即:2019年、是、祖國、70、周年。

這就是我們所說的分詞,也是最小單元,因?yàn)槿绻覀儼阉俨鸱殖鋈サ脑?,那就沒有什么實(shí)際意義了。

Javascript 代碼中的語法單元主要包括以下這么幾種:

  • 關(guān)鍵字:例如 varlet、const
  • 標(biāo)識符:沒有被引號括起來的連續(xù)字符,可能是一個變量,也可能是 if、else 這些關(guān)鍵字,又或者是 true、false 這些內(nèi)置常量
  • 運(yùn)算符: +-、 *、/
  • 數(shù)字:像十六進(jìn)制,十進(jìn)制,八進(jìn)制以及科學(xué)表達(dá)式等語法
  • 字符串:因?yàn)閷τ嬎銠C(jī)而言,字符串的內(nèi)容會參與計算或顯示
  • 空格:連續(xù)的空格,換行,縮進(jìn)等
  • 注釋:行注釋或塊注釋都是一個不可拆分的最小語法單元
  • 其他:大括號、小括號、分號、冒號等

如果我們以最簡單的復(fù)制語句為例的話,如下?

    var a = 1;

通過分詞,我們可以得到如下結(jié)果:

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "a"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Numeric",
        "value": "1"
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

為了方便,我直接在 esprima/parser 這個網(wǎng)站生成分詞的~

什么是語法分析?

上面我們已經(jīng)得到了我們分詞的結(jié)果,需要將詞匯進(jìn)行一個立體的組合,確定詞語之間的關(guān)系,確定詞語最終的表達(dá)含義。

簡單來說語法分析是對語句和表達(dá)式識別,確定之前的關(guān)系,這是個遞歸過程。

上面我們通過語法分析,可以得到如下結(jié)果:

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "a"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 1,
                        "raw": "1"
                    }
                }
            ],
            "kind": "var"
        }
    ],
    "sourceType": "script"
}

這就是 var a = 1 所轉(zhuǎn)換的 AST;

這里推薦一下astexplorer AST的可視化工具,astexplorer,可以直接進(jìn)行對代碼進(jìn)行AST轉(zhuǎn)換~

AST 到底怎么用?

上面畫了很大篇幅聊了聊AST是什么以及它是如何生成的,說到底,還是不知道AST這玩意有啥用,到底怎么使用。。

ok~ 接下來我們來一起見證奇跡。

我相信大部分同學(xué)對 babel 這個庫不陌生,現(xiàn)在的做前端模塊化開發(fā)過程中中一定少不了它,因?yàn)樗梢詭湍銓?ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中,你不用為新語法的兼容性考慮~

而實(shí)際上呢,babel 中的很多功能都是靠修改 AST 實(shí)現(xiàn)的。

首先,我們來看一個簡單的例子,我們?nèi)绾螌?es6 中的 箭頭函數(shù) 轉(zhuǎn)換成 es5 中的 普通函數(shù),即:

const sum=(a,b)=>a+b;

我們?nèi)绾螌⑸厦婧唵蔚?sum 箭頭函數(shù)轉(zhuǎn)換成下面的形式:

const sum = function(a,b){
    return a+b;
}

想想看,有什么思路?

如果說你不了解 AST 的話,這無疑是一個很困難的問題,根本無從下手,如果你了解 AST 的話,這將是一個非常 easy 的例子。

接下來我們看看如何實(shí)現(xiàn)?

安裝依賴

需要操作 AST 代碼,這里,我們需要借助兩個庫,分別是 @babel/corebabel-types

其中 @babel/corebabel 的核心庫,用來實(shí)現(xiàn)核心轉(zhuǎn)換引擎,babel-types 類型判斷,用于生成AST零部件

cd 到一個你喜歡的目錄,通過 npm -y init 初始化操作后,通過 npm i @babel/core babel-types -D 安裝依賴

目標(biāo)分析

要進(jìn)行轉(zhuǎn)換之前,我們需要進(jìn)行分析,首先我們先通過 astexplorer 查看目標(biāo)代碼跟我們現(xiàn)在的代碼有什么區(qū)別。

源代碼的 AST 結(jié)構(gòu)如下:

// 源代碼的 AST
{
    "type": "Program",
    "start": 0,
    "end": 21,
    "body": [
        {
            "type": "VariableDeclaration",
            "start": 0,
            "end": 21,
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "start": 6,
                    "end": 20,
                    "id": {
                        "type": "Identifier",
                        "start": 6,
                        "end": 9,
                        "name": "sum"
                    },
                    "init": {
                        "type": "ArrowFunctionExpression",
                        "start": 10,
                        "end": 20,
                        "id": null,
                        "expression": true,
                        "generator": false,
                        "async": false,
                        "params": [
                            {
                                "type": "Identifier",
                                "start": 11,
                                "end": 12,
                                "name": "a"
                            },
                            {
                                "type": "Identifier",
                                "start": 13,
                                "end": 14,
                                "name": "b"
                            }
                        ],
                        "body": {
                            "type": "BinaryExpression",
                            "start": 17,
                            "end": 20,
                            "left": {
                                "type": "Identifier",
                                "start": 17,
                                "end": 18,
                                "name": "a"
                            },
                            "operator": "+",
                            "right": {
                                "type": "Identifier",
                                "start": 19,
                                "end": 20,
                                "name": "b"
                            }
                        }
                    }
                }
            ],
            "kind": "const"
        }
    ],
    "sourceType": "module"
}

目標(biāo)代碼的 AST 結(jié)構(gòu)如下:

// 目標(biāo)代碼的 `AST`
{
    "type": "Program",
    "start": 0,
    "end": 48,
    "body": [
        {
            "type": "VariableDeclaration",
            "start": 0,
            "end": 48,
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "start": 6,
                    "end": 47,
                    "id": {
                        "type": "Identifier",
                        "start": 6,
                        "end": 9,
                        "name": "sum"
                    },
                    "init": {
                        "type": "FunctionExpression",
                        "start": 12,
                        "end": 47,
                        "id": null,
                        "expression": false,
                        "generator": false,
                        "async": false,
                        "params": [
                            {
                                "type": "Identifier",
                                "start": 22,
                                "end": 23,
                                "name": "a"
                            },
                            {
                                "type": "Identifier",
                                "start": 25,
                                "end": 26,
                                "name": "b"
                            }
                        ],
                        "body": {
                            "type": "BlockStatement",
                            "start": 28,
                            "end": 47,
                            "body": [
                                {
                                    "type": "ReturnStatement",
                                    "start": 32,
                                    "end": 45,
                                    "argument": {
                                        "type": "BinaryExpression",
                                        "start": 39,
                                        "end": 44,
                                        "left": {
                                            "type": "Identifier",
                                            "start": 39,
                                            "end": 40,
                                            "name": "a"
                                        },
                                        "operator": "+",
                                        "right": {
                                            "type": "Identifier",
                                            "start": 43,
                                            "end": 44,
                                            "name": "b"
                                        }
                                    }
                                }
                            ]
                        }
                    }
                }
            ],
            "kind": "const"
        }
    ],
    "sourceType": "module"
}

其中里面的 startend 我們不用在意,其只是為了標(biāo)記其所在代碼的位置。

我們關(guān)心的是 body 里面的內(nèi)容,通過對比,我們發(fā)現(xiàn)主要不同就在于 init 這一段,一個是 ArrowFunctionExpression , 另一個是 FunctionExpression , 我們只需要將 ArrowFunctionExpression 下的內(nèi)容改造成跟 FunctionExpression 即可。

小試牛刀

我們建一個 arrow.js 的文件,引入上面的兩個庫,即

//babel 核心庫,用來實(shí)現(xiàn)核心轉(zhuǎn)換引擎
const babel = require('@babel/core')
//類型判斷,生成AST零部件
const types = require('babel-types')

//源代碼
const code = `const sum=(a,b)=>a+b;` //目標(biāo)代碼 const sum = function(a,b){ return a + b }

這里我們需要用到 babel 中的 transform 方法,它可以將 js 代碼轉(zhuǎn)換成 AST ,過程中可以通過使用 pluginsAST 進(jìn)行改造,最終生成新的 ASTjs 代碼,其整個過程用網(wǎng)上一個比較貼切的圖就是:

關(guān)于 babel transform 詳細(xì)用法,這里不多做討論,感興趣的話可以去官網(wǎng)了解~

其主要用法如下:

//transform方法轉(zhuǎn)換code
//babel先將代碼轉(zhuǎn)換成ast,然后進(jìn)行遍歷,最后輸出code

let result = babel.transform(code,{
    plugins:[
        {
            visitor
        }
    ]
})

其核心在于插件 visitor 的實(shí)現(xiàn)。

它是一個插件對象,可以對特定類型的節(jié)點(diǎn)進(jìn)行處理,這里我們需要處理的節(jié)點(diǎn)是ArrowFunctionExpression,它常見的配置方式有兩種:

一種是單一處理,結(jié)構(gòu)如下,其中 path 代表當(dāng)前遍歷的路徑 path.node 代表當(dāng)前變量的節(jié)點(diǎn)

let visitor = {
    ArrowFunctionExpression(path){
    
    }
}

另一種是用于輸入和輸出雙向處理,結(jié)構(gòu)如下,參數(shù) node 表示當(dāng)前遍歷的節(jié)點(diǎn)

let visitor = {
     ArrowFunctionExpression : {
        enter(node){
            
        },
        leave(node){
            
        }
    }
}

這里我們只需要處理一次,所以采用第一種方式。

通過分析目標(biāo)代碼的 AST,我們發(fā)現(xiàn),需要一個 FunctionExpression , 這時候我們就需要用到 babel-types ,它的作用就是幫助我們生產(chǎn)這些節(jié)點(diǎn)

我們通過其 npm 包的文檔查看,構(gòu)建一個 FunctionExpression 需要的參數(shù)如下:

functionExpression

參照 AST 我們可以看到其 idnullparams 是原 ArrowFunctionExpression 中的 params,body 是一個blockStatement,我們也可以通過查看 babel-types 文檔,用 t.blockStatement(body, directives) 來創(chuàng)建,依次類推,照貓畫虎,最終得到的結(jié)果如下:

    //原 params
    let params = path.node.params;
    //創(chuàng)建一個blockStatement
    let blockStatement = types.blockStatement([
        types.returnStatement(types.binaryExpression(
            '+',
            types.identifier('a'),
            types.identifier('b')
        ))
    ]);
    //創(chuàng)建一個函數(shù)
    let func = types.functionExpression(null, params, blockStatement, false, false);

最后通過 path.replaceWith(func); 將其替換即可;

完成代碼如下:

//babel 核心庫,用來實(shí)現(xiàn)核心轉(zhuǎn)換引擎
const babel = require('@babel/core')
//類型判斷,生成AST零部件
const types = require('babel-types')

//源代碼
const code = `const sum=(a,b)=>a+b;` //目標(biāo)代碼 const sum = function(a,b){ return a + b }

//插件對象,可以對特定類型的節(jié)點(diǎn)進(jìn)行處理
let visitor = {
    //代表處理 ArrowFunctionExpression 節(jié)點(diǎn)
    ArrowFunctionExpression(path){
        let params = path.node.params;
        //創(chuàng)建一個blockStatement
        let blockStatement = types.blockStatement([
            types.returnStatement(types.binaryExpression(
                '+',
                types.identifier('a'),
                types.identifier('b')
            ))
        ]);
        //創(chuàng)建一個函數(shù)
        let func = types.functionExpression(null, params, blockStatement, false, false);
        //替換
        path.replaceWith(func);
    }
}

//transform方法轉(zhuǎn)換code
//babel先將代碼轉(zhuǎn)換成ast,然后進(jìn)行遍歷,最后輸出code
let result = babel.transform(code,{
    plugins:[
        {
            visitor
        }
    ]
})

console.log(result.code);

執(zhí)行代碼,打印結(jié)果如下:

result

至此,我們的函數(shù)轉(zhuǎn)換完成,達(dá)到預(yù)期效果。

怎么樣,有沒有很神奇!!

其實(shí)也沒那么復(fù)雜,主要是要分析其 AST 的結(jié)構(gòu),只要弄懂我們需要干什么,那么放手去做就是~

pass:細(xì)心的同學(xué)發(fā)現(xiàn),上面的代碼其實(shí)可以優(yōu)化的,因?yàn)槲覀兊?returnStatement 其實(shí)也是跟源代碼的 returnStatement 是一致的,我們只需要拿來復(fù)用即可,因此上述的代碼還可以改成下面這樣:

    let blockStatement = types.blockStatement([
        types.returnStatement(path.node.body)
    ]);

趁熱打鐵

上面剛剛認(rèn)識了如何使用 AST 進(jìn)行代碼改造,不妨趁熱打鐵,再來試試下面這個問題。

如何將 es6 中的 class 改造成 es5function 形式~

源代碼

// 源代碼
class Person {
  constructor(name) {
      this.name=name;
  }
  sayName() {
      return this.name;
  }
}

目標(biāo)代碼

// 目標(biāo)代碼

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function () {
    return this.name;
};

有了上面的基礎(chǔ),照貓畫虎即可,這里我將不在贅述,過程很重要,這里我僅粘貼核心的轉(zhuǎn)換代碼,以供參考。

ClassDeclaration (path) {
    let node = path.node; //當(dāng)前節(jié)點(diǎn)
    let id = node.id;   //節(jié)點(diǎn)id
    let methods = node.body.body; // 方法數(shù)組
    let constructorFunction = null; // 構(gòu)造方法
    let functions = []; // 目標(biāo)方法
    
    methods.forEach(method => {
        //如果是構(gòu)造方法
        if ( method.kind === 'constructor' ) {
            constructorFunction = types.functionDeclaration(id, method.params, method.body, false, false);
            functions.push(constructorFunction)
        } else {
            //普通方法
            let memberExpression = types.memberExpression(types.memberExpression(id, types.identifier('prototype'), false), method.key, false);
            let functionExpression = types.functionExpression(null, method.params, method.body, false, false);
            let assignmentExpression = types.assignmentExpression('=', memberExpression, functionExpression);
            functions.push(types.expressionStatement(assignmentExpression));
        }
    })
    //判斷,replaceWithMultiple用于多重替換
    if(functions.length === 1){
        path.replaceWith(functions[0])
    }else{
        path.replaceWithMultiple(functions)
    }
}

總結(jié)

日常工作中,我們大多數(shù)時候關(guān)注的只是 js 代碼本身,而沒有通過 AST 去重新認(rèn)識和詮釋自己的代碼~

本文也只是通過對 AST 的一些介紹,起一個拋磚引玉的作用,讓你對它 有一個初步的認(rèn)識,對它不在感覺那么陌生。

對代碼的追求和探索是無止境的~

如果你愿意,你可以通過它構(gòu)建任何你想要的js代碼~

加油!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容