Javascript抽象語法樹下篇(實踐篇)

做者:陳曉強html

上篇已經對AST基礎作了介紹,本篇介紹AST的運用vue

AST應用的三個要點

  1. 須要一個解析器,將代碼轉換爲AST
  2. 須要一個遍歷器,可以遍歷AST,並可以方便的對AST節點進行增刪改查等操做
  3. 須要一個代碼生成器,可以將AST轉換爲代碼

esprima與babel

經常使用的知足上述3個要點的工具包有兩個,一個是esprima,一個是babelnode

esprima相關包及使用以下webpack

const esprima = require('esprima');   // code => ast
const estraverse = require('estraverse'); //ast遍歷
const escodegen = require('escodegen'); // ast => code
let code = 'const a = 1';
const ast = esprima.parseScript(code);
estraverse.traverse(ast, {
    enter: function (node) {
        //節點操做
    }
});
const transformCode = escodegen.generate(ast);
複製代碼

babel相關包及使用以下git

const parser = require('@babel/parser');  //code => ast
const traverse = require('@babel/traverse').default; // ast遍歷,節點增刪改查,做用域處理等
const generate = require('@babel/generator').default; // ast => code
const t = require('@babel/types'); // 用於AST節點的Lodash式工具庫,各節點構造、驗證等
let code = 'const a = 1';
let ast = parser.parse(sourceCode);
traverse(ast, {
  enter (path) { 
    //節點操做
  }
})
const transformCode = escodegen.generate(ast);
複製代碼

目前babel無論是從生態上仍是文檔上比esprima要好不少,所以推薦你們使用babel工具,本文示例也使用babel來作演示。github

使用babel工具操做AST

如上一章節所示web

  • @babel/parser用於將代碼轉換爲AST
  • @babel/traverse用於對AST的遍歷,包括節點增刪改查、做用域等處理
  • @babel/generator 用於將AST轉換成代碼
  • @babel/types 用於AST節點操做的Lodash式工具庫,各節點構造、驗證等

更多api詳見babel手冊[1]小程序

下面經過簡單案例來介紹如何操做AST,注意案例只是示例,因爲篇幅對部分邊界問題只會註釋說明,實際開發過程當中須要考慮周全。api

案例1:去掉代碼中的console.log()

實現代碼微信

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` function square(n) { console.log(n); console.warn(n); return n * n; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
 CallExpression(path) {
  let { callee } = path.node;
  if (callee.type === ‘MemberExpression’ && callee.object.name === ‘console’ && callee.property.name === ‘log’ ) {
   path.remove(); // 注意考慮對象掛載的識別,如global.console.log(),此時remove後剩下global.,會致使語法錯誤,此時能夠判斷父節點類型來排除
  }
 }
})
console.log(generate(ast).code);
複製代碼

處理結果

function square(n) {
- console.log(n);
  console.warn(n);
  return n * n;
}
複製代碼

此案例涉及知識點

  1. 如何經過 traverse遍歷特定節點
  2. 識別出console.log()在規範中屬於函數調用表達式,節點類型爲CallExpression
  3. console.log自己即callee是在對象console上的一個方法,所以console.log是一個成員表達式,類型爲MemberExpression
  4. MemberExpression根據規範有一個object屬性表明被訪問的對象,有一個property表明訪問的成員。
  5. 經過path.remove()api能夠對節點進行刪除。
  6. 能夠經過https://astexplorer.net/ 來輔助對代碼節點的識別。注意選擇babylon7,即babe7,對應@babel/parser

案例2:變量混淆

實現代碼

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` function square(number) { console.warn(number); return number * number; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
  FunctionDeclaration(path) {
    let unia = path.scope.generateUidIdentifier("a");
    path.scope.rename("number",unia.name);
 }
})

console.log(generate(ast).code);
複製代碼

處理結果

-function square(number) {
+ function square(_a) {
- console.warn(number);
+ console.warn(_a);
- return number * number;
+ return _a * _a;
}
複製代碼

此案例涉及知識點

  1. path.scope保存了當前做用域的相關信息
  2. 能夠經過api對做用域內的變量名進行批量修改操做
  3. 經過path.scope能夠得到當前做用域惟一標識符,避免變量名衝突

案例3:轉換箭頭函數並去掉未使用參數

實現代碼

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },200) }); `
let ast = parser.parse(sourceCode);
traverse(ast, {
  ArrowFunctionExpression (path) { 
    let { id, params, body } = path.node;
    for(let key in path.scope.bindings){   //注意考慮箭頭函數的this特性,若發現函數體中有this調用,則須要在當前做用域綁定其父做用域的this
      if(!path.scope.bindings[key].referenced){
        params = params.filter(param=>{
          return param.name!==key;
        })
      }
    }
  path.replaceWith(t.functionExpression(id, params, body)); 
  }
})

console.log(generate(ast).code);
複製代碼

處理結果

-new Promise((resolve,reject)=>{
+new Promise(function(resolve){
- setTimeout(()=>{
+ setTimeout(function(){
    resolve(1);
  },200)
});
複製代碼

此案例涉及知識點

  1. 箭頭函數節點:ArrowFunctionExpression
  2. 經過path.scope能夠識別變量引用狀況,是否有被引用,被哪些路徑引用
  3. 經過@babel/types能夠很方便的構建任意類型節點
  4. 經過path.replaceWith()能夠進行節點替換

案例4:京東購物小程序的Tree-shaking

刪掉小程序中的冗餘代碼,部分實現代碼示例以下

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` export function square (x) { return x * x; } export function cube (x) { return x * x * x; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
  ExportNamedDeclaration (path) {
    let unused = ['cube']   // 藉助webpack,咱們能得到導出的方法中,哪些是沒有被使用過的
    let { declaration = {} } = path.node;
    if (declaration.type === 'FunctionDeclaration') {
      unused.forEach(exportItem => {
        // references=1表示僅有一次引用,即export的引用,沒有在別處調用
        if (declaration.id.name === exportItem && path.scope.bindings[exportItem].references === 1) {
          path.remove();
        }
      });
    }
  }
})

console.log(generate(ast).code);
複製代碼

處理結果

export function square (x) {
    return x * x;
}
-export function cube (x) {
- return x * x * x;
-}
複製代碼

此案例涉及知識點

  1. export節點:ExportNamedDeclaration

案例5:將代碼轉換成svg流程圖

此案例是git上一個比較有意思的開源項目,經過AST將代碼轉換爲svg流程圖,詳見js-code-to-svg-flowchart[2]

能夠體驗一下:demo[3]

經過以上示例,能夠看到經過AST咱們能夠對代碼任意蹂躪,作出不少有意思的事情

AST在其餘語言的應用

除了Javascript,其餘語言如HTML、CSS、SQL等也有普遍的AST應用。以下圖,能夠在這裏找到對應語言的解析器,開啓AST之門。

其餘AST

結語

在上述AST網站中,能夠看到HTML的解析器有個vue選項,讀過vue源碼的同窗應該知道vue模板在轉換成HTML以前會先將模板轉換成AST而後生成render function進而生成VirtualDOM。咱們平時開發對AST使用比較少,但其實處處都能見到AST的影子:babel、webpack、eslint、taro等等。但願能拋磚引玉,使同窗們在各自團隊產出更多基於AST的優秀工具、項目。

References
[1] babel手冊
[2] js-code-to-svg-flowchart
[3] demo


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam):

WecTeam
相關文章
相關標籤/搜索