從現在起-徹底學會 js ast

0afc5d1784e3414ad8aa93c6b74929bf.png
這是一棵樹嘛

直奔主題

抽象語法樹是js代碼另一種結構映射,可以將js拆解成AST,也可以把AST轉成源代碼。這中間的過程就是我們的用武之地。 利用 抽象語法樹(AST) 可以對你的源代碼進行修改、優化,甚至可以打造自己的編譯工具。其實有點類似babel的功能。

AST高深的狠嚇人?

AST很簡單,並沒有你想象的那樣高深。很多地方都把這個技術給誇大了,什麼編譯原理,抽象語法樹 光看這名字就覺得嚇人。當然一項技術總歸要起個名字,就像給自己的孩子取名字,肯定要起一個高大上,深有寓意的名字。所以,名字只是一個代號。從名字來看就會讓很多人望而卻步。但是ast超級簡單,但是功能超級強大。

我們能用這個技術做很多有意思的東西,只要你能想到的。

本文術道結合,讓你感受到ast的有趣和簡單,從此愛上ast,還能根據自己的需要打造自己的編譯器。

什麼是AST?

ast全稱是abstract syntax tree,翻譯過來叫-抽象語法樹。其實這含兩個意思,一個是“抽象”,一個是“樹”。抽象表示把js代碼進行了結構化的轉化,轉化爲一種數據結構。這種數據結構其實就是一個大的json對象,json我們都熟悉,他就像一顆枝繁葉茂的樹。

有樹根,有樹幹,有樹枝,有樹葉.無論多小多大,都是一棵完整的樹。

如何生成AST?

你可以大致的想一下如果親自實現把js代碼轉換成結構化的數據我們應該怎麼做?

有點像小時候拆解自己的玩具,每個零件之間都有着從屬關係。

對於如何生成ast,我們可能會想到分析js代碼的規則使用字符串處理、正則匹配等方法,如果對簡單的代碼處理我們是可以實現的。但是如果能夠對隨意的一段代碼進行處理那就需要考慮非常多的情況。具體如何實現咱們不必過於糾結,這也不是重點。

但最終的實現裏我們能想到方法基本都會被用到。我們可以簡化理解,也就是對js代碼經過了一系列的加工處理,變成了一堆零件或者食材(像老媽給我們做的香噴噴的飯菜,但前提是先準備好菜)。

這個拆解的過程可能較爲複雜,所以我們需要用現成方法,直接拿過來用就可以了。

所以我們需要用到esprima、UglifyJS等庫,做菜的食材有很多種,所以會存在很多這樣的三方庫,而我們會使用其中一種就可以了。

先使用esprima 種菜,體會一下

種子:

//源代碼
function fun(a,b){
  
}

成熟:

{
            "type": "FunctionDeclaration",//函數聲明
            "id": {
                "type": "Identifier",//標識符
                "name": "fun" //函數名稱
            },
            "params": [//函數參數
                {
                    "type": "Identifier",//參數標識符
                    "name": "a"//參數名稱
                },
                {
                    "type": "Identifier",
                    "name": "b"
                }
            ],
            "body": {//函數體
                "type": "BlockStatement",//語句塊兒
                "body": []//具體內容爲空,因爲是空方法
            }
      }

有了AST能做什麼?

到這一步你已經可以把js代碼轉換成一棵結構化的樹了,那下一步要做什麼呢? 比如在沒有樹的情況下,你要對代碼裏的某個代碼進行替換。要把所有 console.log給註釋掉或者刪除,你可能會使用IDE的查找替換或者用node寫一個方法,讀取文件然後查找替換。

這種方式不夠安全也不夠科學,稍有不慎就會把代碼給搞壞了。

但這個時候你有了結構化代碼樹,是不是隻要對這棵樹進行修修剪剪然後把這棵樹轉換成爲js代碼就可以了呢?

答案:肯定是可以的。因爲樹已經發生了變化,修改了樹就相當於修改了源碼。

怎樣操作這棵樹呢?我想你應該已經知道了,就是對這json對象進行操作,方法就多了去了,前提是你得有一點點js基礎。

又一個問題,怎樣把樹再轉成代碼?

腦洞打開,用遞歸加字符串拼接,這個方法應該是可以的。

但是這棵樹不是你生成的,結構特點你並不清楚,成千上萬個節點呢?怎麼拼接?真要幹,那可能得搞得流鼻血。

這就像是食材準備好了,轉換成源碼的過程就是炒菜的過程。具體的轉源碼的原理不多說,也不必糾結。使用現成的方法就可以,所以要用到estraverse,escodegen這兩個庫。

estraverse 可以遍歷樹的所有節點,省去你對樹的遞歸遍歷

escodegen 可以把樹再加工轉成源代碼

過程總結

到這裏始終都沒有提到任何代碼,只是理論了一番,但是相信你已經理解了ast以及ast的作用。然後在述說過程中引出了3個庫,有了這三個庫就可以對你的js代碼進行多樣化處理,只要你能想到的。

看圖理解整個處理過程:

4226719341e4d0193ae3882e3dfc30e6.png@wm_2,t_55m+5a625Y+3L+WJjeerr+efpemBkw==,fc_ffffff,ff_U2ltSGVp,sz_20,x_13,y_13

這個過程簡單,清晰,所以說ast簡單、有趣、好玩。因爲此刻代碼可以被你任意的蹂躪了。

實例應用

說的再清楚都不夠直觀,畢竟都是腦補,不如看代碼來的爽快。

這裏就拿日常編碼中的一些小問題舉例,來演示一下AST的使用。

  1. 把 == 改爲全等 ===
  2. 把parsetInt不標準的調用改爲標準用法 parseInt(a)-> parseInt(a,10)

這裏我使用esprima的官方工具生成了ast,工具地址http://esprima.org/demo/parse...

看下要處理的源碼:

//源碼
function fun1() {
    console.log('fun1');
}
function fun2(opt) {
    if (opt.status == 1) {
        console.log('1');
    }
    if (opt.status == 2) {
        console.log('2');
    }
}
function fun3(age) {
    if (parseInt(age) >= 18) {
        console.log('ok 你已經成年');
    }
}

轉成ast,由於轉成樹後結構非常大,所以這裏我只貼了一部分,你也可以到工具頁面自己生成下。

{
    "type": "Program",
    "body": [
        {
            "type": "FunctionDeclaration",
            "id": {
                "type": "Identifier",
                "name": "fun1"
            },
            "params": [],
            "body": {
                "type": "BlockStatement",
                "body": [
                    {
                        "type": "ExpressionStatement",
                        "expression": {
                            "type": "CallExpression",
                            "callee": {
                                "type": "MemberExpression",
                                "computed": false,
                                "object": {
                                    "type": "Identifier",
                                    "name": "console"
                                },
                                "property": {
                                    "type": "Identifier",
                                    "name": "log"
                                }
                            },
                            "arguments": [
                                {
                                    "type": "Literal",
                                    "value": "fun1",
                                    "raw": "'fun1'"
                                }
                            ]
                        }
                    }
                ]
            },
            "generator": false,
            "expression": false,
            "async": false
        }
    ]
}

ast看上去結構複雜,盯着仔細看後基本都能看懂。所有的代碼都在特定的節點裏面。具體的這裏就不介紹了,可以到上面的工具地址去觀察不同的ast結構。總之這就是一個對象,只要你能對這個對象進行修改、添加、刪除即可。

開始實現以上功能
init

//引入工具包
const esprima = require('esprima');//JS語法樹模塊
const estraverse = require('estraverse');//JS語法樹遍歷各節點
const escodegen = require('escodegen');//JS語法樹反編譯模塊
//獲�取代碼ast
const AST = esprima.parseScript(jsCode);

/**
 * 
 * @param {遍歷語法樹} ast 
 */
function walkIn(ast){
    estraverse.traverse(ast, {
        enter: (node) => {
            toEqual(node);//把 == 改爲全等 ===
            setParseint(node); //parseInt(a)-> parseInt(a,10)
        }
    });
}

2.把 == 改爲全等 ===

/**
 * 設置全等
 */
function toEqual(node) {
    if (node.operator === '==') {
        node.operator = '===';
    }
}
  1. 把parseInt改成標準調用
/**
 * 把parseint改爲標準方法
 * @param {節點} node 
 */
function setParseint(node) {
    //判斷節點類型 方法名稱,方法的參數的數量,數量爲1就增加第二個參數
    if (node.type === 'CallExpression' && node.callee.name === 'parseInt' && node.arguments.length===1){

        node.arguments.push({//增加參數,其實就是數組操作
            "type": "Literal",
            "value": 10,
            "raw": "10"
        });
    }
}

//生成目標代碼
const code = escodegen.generate(ast);
//寫入文件.....
//....你懂的

代碼不多,需求簡單,但已足夠能說明整個處理過程以及ast的強大。 ast的節點很多,有些凌亂,送你一首歌【汪峯的無所謂】,操作的時候只要關心你自己的需求就可以,不需要對所有的節點都搞明白。按需處理就可以。

AST技術的應用

雖然平時用不到ast,但又時刻都在使用ast技術。家喻戶曉、無人不知的babel,webpack,還有jd taro等都把ast用的淋漓盡致,脫離了ast他們就跪了。

AST這麼簡單,好沒技術含量
AST沒有技術含量嗎?怎麼可能呢,如果真這麼認爲怕是會被笑掉大牙的。如果僅僅停留在使用層面的話,理解到這步已經基本可以了,只要是你能對這棵樹做修剪就可以對源代碼做手腳。

另外ast怎樣生成的?怎樣把ast轉換成源碼的?這就有點高深了。會使用就像是在山腳下能看到的風景有限,理解了背後原理機制就像是爬上了山頂,別樣的風景盡收眼底。不過上不上山看個人興趣,有興趣的同學可以去看源碼、做研究,這裏就不再多說,因爲我也不知道。哈哈哈

總結

本文主要介紹了

什麼是ast:

ast其實就把js代碼進行抽象爲一種json結構;

ast的用途:

利用ast可以方便的優化和修改代碼,還能打造自己的編譯器;

然後通過具體的示例演示了怎樣操作ast,最終是希望你能對ast有一個系統全局的認識和理解並能夠利用ast打造自己的編譯工具。

演示代碼下載,歡迎star

https://github.com/bigerfe/fo...

自家觀點,歡迎打臉

原創不易,請多鼓勵

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章