一個簡單的例子看懂抽象語法樹的魔力

本文嘗試以一個最簡單的例子來展示抽象語法樹的魔力。html

主要從六方面闡述:node

  1. 抽象語法樹簡介
  2. 代碼執行的三個步驟
  3. 詞法分析
  4. 語法分析
  5. 一個簡單的小例子
  6. 抽象語法樹的應用

抽象語法樹的簡介

在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。express

上面這是百度百科的定義,一如既往的讓人摸不着頭腦。npm

咱們總結一下百度百科的定義:編程

  1. 抽象語法樹是一顆樹
  2. 樹的每一個節點都表示了源代碼的一種語法結構

上面這段總結有幾個關鍵詞:抽象語法樹、樹、節點、源代碼、語法結構,其中語法結構是比較難理解的,那麼什麼是語法結構呢?segmentfault

舉幾個簡單的例子:數組

//變量聲明
var a = 1;
複製代碼
//循環
while(true){
    console.log(1);
}
複製代碼
//判斷
if(true){
    console.log(1);
}
複製代碼
//函數聲明
function a(){
    console.log(1);
}
複製代碼

以上這些例子是js的語法聲明(statement),這種聲明就能夠當作js的語法結構! 也就是說抽象語法樹的每一個節點都在描述這種結構。bash

好比說一個節點是變量聲明,那麼這個節點的子節點都會去描述變量聲明的具體內容:變量名是什麼,變量是什麼類型,變量的初始值是什麼等等。編程語言

就是這樣一個一個的聲明,構成了抽象語法樹。ide

代碼執行的三個步驟

從js程序到機器可執行的機器碼須要經歷兩個階段:

  • 語法檢查
  • 編譯運行

語法檢查又分爲語法分析和詞法分析,因此分紅三個步驟就是:

  • 詞法分析
  • 語法分析
  • 編譯運行

這裏先簡單介紹下每一個階段都幹了什麼活:

第一步:詞法分析,也叫作掃描scanner。它讀取咱們的代碼,而後把它們按照預約的規則合併成一個個的標識Tokens(type 和 value )。這個階段,它會移除空白符,註釋等。最後,整個代碼將被分割進一個Tokens列表(一個一維數組)。

第二步:語法分析,也叫作解析器。它會將詞法分析出來的Token數組轉化成樹形的表達形式。同時,驗證語法,語法若是有錯的話,拋出語法錯誤。

第三步:編譯階段,也叫編譯器。這個階段會處理AST,生成機器可執行的機械碼。

詞法分析

先以一個簡單的例子看下token序列長什麼樣

var a = 1;
複製代碼

他的token長這樣

其實就是一個一維數組,裏面有一些對象用於描述每一個單詞。

我整理了下常見的type:

  • Keyword (關鍵詞)
  • Identifier (標識符)
  • Punctuator (標點符號)
  • Numberic(數字)
  • String (字符串)
  • Boolean(布爾)
  • Null(空值)

語法分析

詞法分析由源代碼生成了Token序列,語法分析則是由Token序列生成了抽象語法樹。

仍是看上一個例子:

var a = 1;
複製代碼

他的抽象語法樹長這樣:

一、先看最外層的三個屬性

  • type(表示是一段程序代碼)
  • body(代碼的具體內容)
  • sourceType(表示語言的種類)

二、再看body裏面的具體內容,body是一個數組,這是由於程序可能有多個內容塊(statement),每一個內容塊用一個對象表示。

三、再看每一個內容塊的內容

  • type(表示這個內容塊是幹什麼的)
  • declarations(乘裝變量內容的塊,能夠看到這個塊也是一個數組,由於變量聲明可能生命多個,因此一個生命對應一個對象 例如 var a=1,b=2;) kind(關鍵字)

四、再看declarations裏面對象裏面的內容

  • type (聲明的類型是個變量)
  • id(表示變量名)
  • init(表示爲這個變量設置的初值)

上面提到statement,statement有不少類型,好比說變量聲明,函數定義,if語句,while循環,等都是一個statement,你們若是想看更多的類型,點擊這裏

一個超級簡單的例子

好了,說了這麼多,終於要寫代碼了。

這個例子實現的功能:

  • 將源代碼中的 == 變成 ===
  • 將源代碼中的 var 變成 let
  • 將源代碼中的 console註釋掉

這個例子用到的工具:

  • Esprima (將源代碼轉化爲ast)
  • Estraverse(遍歷語法樹)
  • Escodegen(講語法書反編譯爲js代碼)

初始化一個項目

npm init
複製代碼

安裝用到的依賴包

npm install esprima estraverse escodegen --save
複製代碼

新建index.js入口文件 和 originCode.js 源代碼文件

在 originCode.js 中輸入要轉換的源代碼

function fun() {
    var opt = 1;
    console.log(1);
    if (opt == 1) {
        console.log(2);
    }
}
複製代碼

在 index.js 中實現咱們的功能

let fs  = require('fs');
const esprima = require('esprima');//將JS代碼轉化爲語法樹模塊
const estraverse = require('estraverse');//JS語法樹遍歷各節點
const escodegen = require('escodegen');//將JS語法樹反編譯成js代碼模塊

/**
* 由源代碼獲得抽象語法樹
*/
function  getAst(jsFile) {
    let jsCode;
    return new Promise((resolve)=>{
        fs.readFile(jsFile, (error, data) => {
            jsCode = data.toString();
            resolve(esprima.parseScript(jsCode));
        });
    });
}

/**
 * 設置全等
 */
function setEqual(node) {
    if (node.operator === '==') {
        node.operator = '===';
    }
}

/**
 * 刪除console
 */
function delConsole(node) {
    if (node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.object.name==='console') {
        node.expression.callee.object.name = '//console';
    }
}

/**
 * 把var變成let
 */
function setLet(node){
    if(node.kind === 'var'){    
        node.kind = 'let';
    }
}

/**
 * 遍歷語法樹
 */
function travel(ast){
    estraverse.traverse(ast, {
        enter: (node) => {
            setEqual(node);
            setLet(node);
            delConsole(node);
        }
    });
}

/**
* 生成文件
*/
function  writeCode(file,data) {
    fs.writeFile(file,data,(error)=>{
        console.log(error);
    });
}

/**
* 入口函數
*/
function main(){
    let file = './originCode.js';
    let distFile = './distCode.js';
    getAst(file).then(function(jsCode) {
        travel(jsCode);
        
        // 刪掉 console , 經過parseScript在生成一變ast去掉註釋的內容
        // let distCode = escodegen.generate( esprima.parseScript( escodegen.generate(jsCode)));
        // 註釋 console
        let distCode = escodegen.generate(jsCode);
        console.log('distcode',distCode);

        writeCode(distFile,distCode);
    });
}

main();
複製代碼

而後運行咱們的項目

node index.js
複製代碼

distCode.js的內容已經變成咱們想要的了

function fun() {
    let opt = 1;
    //console.log(1);
    if (opt === 1) {
        //console.log(2);
    }
}
複製代碼

抽象語法樹的應用

經過上面這個例子能夠看出,抽象語法樹具備改變源代碼的魔力,這樣的話抽象語法樹的應用就不難總結了。

  • IDE插件,用來檢查語法,高亮語法等功能。
  • 代碼的混淆壓縮,好比UglifyJS2。
  • 代碼轉換工具,好比Webpack、Babel,或者ts轉js等各類代碼規範之間轉換的工具。

推薦閱讀

一個實時轉換ast的網站

13 個示例快速入門 JS 抽象語法樹

抽象語法樹的文檔

相關文章
相關標籤/搜索