本文嘗試以一個最簡單的例子來展示抽象語法樹的魔力。html
主要從六方面闡述:node
在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。express
上面這是百度百科的定義,一如既往的讓人摸不着頭腦。npm
咱們總結一下百度百科的定義:編程
上面這段總結有幾個關鍵詞:抽象語法樹、樹、節點、源代碼、語法結構,其中語法結構是比較難理解的,那麼什麼是語法結構呢?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:
詞法分析由源代碼生成了Token序列,語法分析則是由Token序列生成了抽象語法樹。
仍是看上一個例子:
var a = 1;
複製代碼
他的抽象語法樹長這樣:
一、先看最外層的三個屬性
二、再看body裏面的具體內容,body是一個數組,這是由於程序可能有多個內容塊(statement),每一個內容塊用一個對象表示。
三、再看每一個內容塊的內容
四、再看declarations裏面對象裏面的內容
上面提到statement,statement有不少類型,好比說變量聲明,函數定義,if語句,while循環,等都是一個statement,你們若是想看更多的類型,點擊這裏。
好了,說了這麼多,終於要寫代碼了。
這個例子實現的功能:
這個例子用到的工具:
npm init
複製代碼
npm install esprima estraverse escodegen --save
複製代碼
function fun() {
var opt = 1;
console.log(1);
if (opt == 1) {
console.log(2);
}
}
複製代碼
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
複製代碼
function fun() {
let opt = 1;
//console.log(1);
if (opt === 1) {
//console.log(2);
}
}
複製代碼
經過上面這個例子能夠看出,抽象語法樹具備改變源代碼的魔力,這樣的話抽象語法樹的應用就不難總結了。