抽象語法樹 Abstract syntax tree

什麼是抽象語法樹?

在計算機科學中,抽象語法和抽象語法樹實際上是源代碼的抽象語法結構的樹狀表現形式
在線編輯器javascript

咱們經常使用的瀏覽器就是經過將js代碼轉化爲抽象語法樹來進行下一步的分析等其餘操做。因此將js轉化爲抽象語法樹更利於程序的分析。java

如圖:node

如上圖中的變量聲明語句,轉化爲AST以後就是右圖的樣子。webpack

先來分析一下左圖:git

var 是一個關鍵字es6

AST是一個定義者github

= 是Equal 等號的叫法有不少形式,在後面咱們還會看到web

「is tree」 是一個字符串chrome

;就是 Semicoionexpress

再來對應一下右圖:

首先一段代碼轉化成的抽象語法樹是一個對象,該對象會有一個頂級的type屬性'Program',第二個屬性是body是一個數組。

body數組中存放的每一項都是一個對象,裏面包含了全部的對於該語句的描述信息

type:描述該語句的類型 --變量聲明語句
kind:變量聲明的關鍵字 -- var
declaration: 聲明的內容數組,裏面的每一項也是一個對象
    type: 描述該語句的類型 
    id: 描述變量名稱的對象
        type:定義
        name: 是變量的名字
    init: 初始化變量值得對象
        type: 類型
        value: 值 "is tree" 不帶引號
        row: "\"is tree"\" 帶引號

抽象語法樹有哪些用途?

代碼語法的檢查,代碼風格的檢查,代碼的格式化,代碼的高亮,代碼錯誤提示,代碼自動補全等等

如:JSLint、JSHint 對代碼錯誤或風格的檢查,發現一些潛在的錯誤
IDE的錯誤提示,格式化,高亮,自動補全等等
代碼的混淆壓縮
如:UglifyJS2等

優化變動代碼,改變代碼結構達到想要的結構

代碼打包工具webpack,rollup等等
CommonJS、AMD、CMD、UMD等代碼規範之間的轉化
CoffeeScript、TypeScript、JSX等轉化爲原生Javascript

經過什麼工具或庫來實現源碼轉化爲抽象語法樹?

那就是javascript Parser 解析器,他會把js源碼轉化爲抽象的語法樹。

瀏覽器會把js源碼經過解析器轉化爲抽象語法樹,再進一步轉化爲字節碼或直接生成機器碼

通常來講每個js引擎都會有本身的抽象語法樹格式,chrome的v8引擎,firefox的SpiderMonkey 引擎等等,MDN提供了詳細SpiderMonkey AST format的詳細說明,算是業界的標準。(SpiderMonkey是Mozilla項目的一部分,是一個用C語言實現的JavaScript腳本引擎,爲了在SpiderMonkey中運行JavaScript代碼,應用程序必須有三個要素:JSRuntime,JSContext和全局對象。)

經常使用的javascript Parser

esprima

traceur

acorn

shift

咱們主要拿esprima來舉一個例子

安裝

npm install esprima estraverse escodegen -S

esprima 涉及三個庫名稱和功能以下:

esprima 把源碼轉化爲抽象語法樹

let esprima = require('esprima'); // 引入esprima
    let jsOrigin = 'function eat(){};'; // 定義一個js源碼
    
    let AST = esprima.parse(jsOrigin); // 經過esprima.parse將js源碼轉化爲一個抽象語法樹
    
    console.log(AST); // 打印生成的抽象語法樹
   
    /*Script {
    type: 'Program',// 頂級的type屬性
    body: [ FunctionDeclaration {
            type: 'FunctionDeclaration', // js源碼的類型--是一個函數聲明
            id: [Identifier],
            params: [],
            body: [BlockStatement],
            generator: false, // 是否是generator函數
            expression: false, // 是否是一個表達式
            async: false // 是否是一個異步函數
            },
            EmptyStatement { type: 'EmptyStatement' } 
          ],
    sourceType: 'script' 
    }*/

estraverse 遍歷並更新抽象語法樹

在介紹用法以前咱們先來npm上看一下這個庫,這個庫的下載量竟然500多萬,並且沒有README說明文檔,是否是很牛掰!

在舉例子以前咱們要遍歷抽象語法樹,首先咱們要先了解一下他的遍歷順利

let estraverse = require('estraverse');
    

    estraverse.traverse(AST, {
        enter(node){
            console.log('enter', node.type)
            if(node.type === 'Identifier') {
                node.name += '_enter'
            }
        },
        leave(node){
            console.log('leave', node.type)
            if(node.type === 'Identifier') {
                node.name += '_leave'
            }
        }
    })
    
    // enter Program
    // enter FunctionDeclaration
    // enter Identifier
    // leave Identifier
    // enter BlockStatement
    // leave BlockStatement
    // leave FunctionDeclaration
    // enter EmptyStatement
    // leave EmptyStatement
    // leave Program

經過上面節點類型的打印結果咱們不難看出,咱們的抽象語法樹的每一個節點被訪問了2次,一次是進入的時候,一次是離開的時候,咱們能夠經過下面的圖來更加清楚的理解抽象語法樹的遍歷順序


看完遍歷順序以後,咱們看到代碼中的判斷條件 若是是變量名的話,第一次進入訪問時對這個變量的名稱作了一次修改,當離開的時候也作了一次修改。那接下來咱們要驗證 抽象語法樹種的這個節點的變量名稱 是否修改爲功了呢?咱們有兩種方案,方案一:直接打印抽象語法樹,這個很是簡單再這裏就你介紹了。方案二: 咱們將現有的抽象語法樹轉化成源碼看一下變量名是否變成功 這樣就一目瞭然了。那怎麼將咱們的抽象語法樹還原成源碼呢?這就要引入咱們的第三個庫了 escodegen

escodegen 將抽象語法樹還原成js源碼

let escodegen = require('escodegen');
    
    let originReback = escodegen.generate(AST);
    console.log(originReback);
    // function eat_enter_leave() {};

經過上面還原回來的源碼咱們看到變量名稱確實被更改了。

接下來咱們來探索一下如何用抽象語法樹來將箭頭函數轉化爲普通的函數

咱們都知道es6語法轉es5的語法咱們用的是babel,讓咱們接下來就看一下 babel是如何將箭頭函數轉化爲普通函數的。

第一步須要使用babel的兩個插件,babel-core 核心模塊 babel-types 類型模塊
npm i babel-core babel-types -S

第一步:咱們先來對比普通函數和箭頭函數的抽象語法樹,經過對比找出其中的不一樣之處,而後在節點能夠複用的前提下,儘量少的改變一下不一樣的地方,從而成功的將箭頭函數轉化爲普通函數。

咱們以這個箭頭函數爲例:

let sum = (a,b) => a+b; 
    ------>
    var sum = function sum(a, b) {
      return a + b;
    };

如上圖所示,普通函數和箭頭函數的AST的不一樣在於init,因此咱們如今要作的是將箭頭函數的arrowFunctionExpression 轉換爲FunctionExpression

利用babel-types生成新的部分的AST語法樹,替換原有的。若是建立某個節點的語法樹,那就在下面的網址上,須要哪一個節點就搜哪一個節點
babel-types

// babel 核心庫,用來實現核心的轉換引擎
    const babel = require('babel-core');
    // 實現類型轉化 生成AST節點
    const types = require('babel-types');
    let code = 'let sum = (a,b) => a+b;';
    let es5Code = function (a,b) {
        return a+b;
    };
    
    // babel 轉化採用的是訪問者模式Visitor 對於某個對象或者一組對象,不一樣的訪問者,產生的結果不一樣,執行操做也不一樣
    
    // 這個訪問者能夠對特定的類型的節點進行處理
    let visitor = {
        ArrowFunctionExpression(path) {
            // 若是這個節點是箭頭函數的節點的話,咱們在這裏進行處理替換工做
            // 1.複用params參數
            let params = path.node.params;
            let blockStatement = types.blockStatement([types.returnStatement(path.node.body)])
            let func = types.functionExpression(null, params, blockStatement, false,false);
            path.replaceWith(func)
    
        }
    };
    
    let arrayPlugin = {visitor};
    
    // babel內部先把代碼轉化成AST,而後進行遍歷
    
    let result = babel.transform(code, {
        plugins: [
            arrayPlugin
        ]
    });
    
    console.log(result.code);
    // let sum = function (a, b) {
    //     return a + b;
    // };

咱們寫一個babel的預計算插件

let code = `const result = 1000 * 60 * 60 * 24`;
    //let code = `const result = 1000 * 60`;
    let babel = require('babel-core');
    let types = require('babel-types');
    //預計算
    let visitor = {
        BinaryExpression(path){
            let node = path.node;
            if(!isNaN(node.left.value)&&!isNaN(node.right.value)){
                let result = eval(node.left.value+node.operator+node.right.value);
                result =  types.numericLiteral(result);
                path.replaceWith(result);
                //若是此表達式的父親也是一個表達式的話,須要遞歸計算
                if(path.parentPath.node.type == 'BinaryExpression'){
                    visitor.BinaryExpression.call(null,path.parentPath);
                }
            }
        }
    }
    let r = babel.transform(code,{
        plugins:[
            {visitor}
        ]
    });
    console.log(r.code);

以上就是我對抽象語法樹的理解,有什麼不正確的地方,懇求斧正。

相關文章
相關標籤/搜索