抽象語法樹 Abstract syntax tree

Macchiato發表於2018-03-26

什麼是抽象語法樹?

在電腦科學中,抽象語法和抽象語法樹其實是原始碼的抽象語法結構的樹狀表現形式 線上編輯器

我們常用的瀏覽器就是通過將js程式碼轉化為抽象語法樹來進行下一步的分析等其他操作。所以將js轉化為抽象語法樹更利於程式的分析。

如圖:

抽象語法樹 Abstract syntax tree

如上圖中的變數宣告語句,轉化為AST之後就是右圖的樣子。

先來分析一下左圖:

var 是一個關鍵字

AST是一個定義者

= 是Equal 等號的叫法有很多形式,在後面我們還會看到

“is tree” 是一個字串

;就是 Semicoion

再來對應一下右圖:

首先一段程式碼轉化成的抽象語法樹是一個物件,該物件會有一個頂級的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次,一次是進入的時候,一次是離開的時候,我們可以通過下面的圖來更加清楚的理解抽象語法樹的遍歷順序

抽象語法樹 Abstract syntax tree
抽象語法樹 Abstract syntax tree

看完遍歷順序之後,我們看到程式碼中的判斷條件 如果是變數名的話,第一次進入訪問時對這個變數的名稱做了一次修改,當離開的時候也做了一次修改。那接下來我們要驗證 抽象語法樹種的這個節點的變數名稱 是否修改成功了呢?我們有兩種方案,方案一:直接列印抽象語法樹,這個非常簡單再這裡就你介紹了。方案二: 我們將現有的抽象語法樹轉化成原始碼看一下變數名是否變成功 這樣就一目瞭然了。那怎麼將我們的抽象語法樹還原成原始碼呢?這就要引入我們的第三個庫了 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;
	};
複製程式碼

抽象語法樹 Abstract syntax tree

如上圖所示,普通函式和箭頭函式的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);

複製程式碼

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

不要厭煩熟悉的事物,每天都進步一點;不要畏懼陌生的事物,每天都學習一點;

相關文章