AST in JS

慕晨同學發表於2018-12-29

寫在前面

Babel,Webpack,vue-cli和esLint等很多的工具和庫的核心都是通過Abstract Syntax Tree抽象語法樹這個概念來實現對程式碼的檢查、分析等操作的。在前端當中AST的使用場景非常廣,比如在vue.js當中,我們在程式碼中編寫的template轉化成render function的過程當中第一步就是解析模版字串生成AST。JS的許多語法為了給開發者更好的程式設計體驗,並不適合不適合程式的理解。所以需要把原始碼轉化為AST來更適合程式分析,瀏覽器的編譯器一般會把原始碼轉化為AST來進行進一步的分析來進行其他操作。通過了解AST這個概念,對深入瞭解前端的一些框架和工具是很有幫助的。

本文將從以下幾部分進行總結:

  1. AST的使用場景
  2. AST的定義
  3. JavaScript Parser(三板斧)
  4. 利用AST轉化箭頭函式
  5. 利用AST實現預計算的babel外掛

AST的使用場景

  • 程式碼語法的檢查、程式碼風格的檢查、程式碼的格式化、程式碼的高亮、程式碼錯誤提示、程式碼自動補全等等
    • 如JSLint、JSHint對程式碼錯誤或風格的檢查,發現一些潛在的錯誤
    • IDE的錯誤提示、格式化、高亮、自動補全等等
  • 程式碼混淆壓縮
    • UglifyJS2等
  • 優化變更程式碼,改變程式碼結構使達到想要的結構
    • 程式碼打包工具webpack、rollup等等
    • CommonJS、AMD、CMD、UMD等程式碼規範之間的轉化
    • CoffeeScript、TypeScript、JSX等轉化為原生Javascript

AST的定義

  • AST的官方定義:

在電腦科學中,抽象語法樹(abstract syntax tree或者縮寫為AST),或者語法樹(syntax tree),是原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。

這是線上的AST轉換器:AST轉換器。程式碼轉化成AST後的格式大致如下圖所示: AST in JS

轉化成AST之後的JSON格式大致為:

{
  "type": "Program",
  "start": 0,
  "end": 16,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 16,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "ast"
      },
      "expression": false,
      "generator": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 14,
        "end": 16,
        "body": []
      }
    }
  ],
  "sourceType": "module"
}
複製程式碼

字串形式的 type 欄位表示節點的型別。比如"BlockStatement","Identifier","BinaryExpression"等。 每一種型別的節點定義了一些屬性來描述該節點型別。然後就可以通過這些節點來進行分析其他操作。

JavaScript Parser(三板斧)

  • JavaScript Parser,把js原始碼轉化為抽象語法樹的解析器。

  • 瀏覽器會把js原始碼通過解析器轉為抽象語法樹,再進一步轉化為位元組碼或直接生成機器碼。

  • 一般來說每個js引擎都會有自己的抽象語法樹格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了詳細SpiderMonkey AST format的詳細說明,算是業界的標準。

JS Parser的三板斧

1.通過 esprima 把原始碼轉化為AST
let esprima = require('esprima');
let code = 'function ast(){}';
let ast = esprima.parse(code);
console.log(ast);
複製程式碼

通過npm i esprima -S安裝之後,執行以上程式碼,會輸出:

Script {
  type: 'Program',
  body:
   [ FunctionDeclaration {
       type: 'FunctionDeclaration',
       id: [Identifier],
       params: [],
       body: [BlockStatement],
       generator: false,
       expression: false,
       async: false } ],
  sourceType: 'script' }
複製程式碼
2.通過 estraverse 遍歷並更新AST
let esprima = require('esprima');
let estraverse = require('estraverse');
let code = 'function ast(){}';
let ast = esprima.parse(code);
estraverse.traverse(ast, {
  enter(node) {
    console.log('enter', node.type)
    if (node.type == 'Indentifier') {
      node.name += 'enter';
    }
  },
  leave(node) {
    console.log('leave', node.type)
    if (node.type == 'Indentifier') {
      node.name += 'leave';
    }
  }
})
console.log(ast);
複製程式碼

通過npm i estraverse -S安裝之後,執行以上程式碼,會輸出:

Script {
  type: 'Program',
  body:
   [ FunctionDeclaration {
       type: 'FunctionDeclaration',
       id: [Identifier],
       params: [],
       body: [BlockStatement],
       generator: false,
       expression: false,
       async: false } ],
  sourceType: 'script' }
複製程式碼
3.通過 escodegen 將AST重新生成原始碼
t esprima = require('esprima');
let estraverse = require('estraverse');
let escodegen = require('escodegen');
let code = 'function ast(){}';
let ast = esprima.parse(code);
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';
    }
  }
});
let result = escodegen.generate(ast)
console.log(result);
複製程式碼

通過npm i escodegen -S安裝完之後,執行以上程式碼,會輸出:

  function ast_enter_leave() {
}
複製程式碼

這樣一來,就把

  function ast() {
}
複製程式碼

修改為了:

  function ast_enter_leave() {
}
複製程式碼

轉化箭頭函式

利用babel-core(babel核心庫,實現核心的轉換引擎)和babel-types(可以實現型別判斷,生成AST節點等)和AST來將

let sum = (a, b) => a + b
複製程式碼

改成為:

let sum = function(a, b) {
  return a + b
}
複製程式碼

實現程式碼如下:

// babel核心庫,實現核心的轉換引擎
let babel = require('babel-core');
// 可以實現型別判斷,生成AST節點等
let types = require('babel-types');

let code = `let sum = (a, b) => a + b`;
// let sum = function(a, b) {
//   return a + b
// }

// 這個訪問者可以對特定型別的節點進行處理
let visitor = {
  ArrowFunctionExpression(path) {
    console.log(path.type);
    let node = path.node;
    let expression = node.body;
    let params = node.params;
    let returnStatement = types.returnStatement(expression);
    let block = types.blockStatement([
        returnStatement
    ]);
    let func = types.functionExpression(null,params, block,false, false);
    path.replaceWith(func);
  }
}

let arrayPlugin = { visitor }
// babel內部會把程式碼先轉成AST, 然後進行遍歷
let result = babel.transform(code, {
  plugins: [
    arrayPlugin
  ]
})
console.log(result.code);
複製程式碼

利用AST實現預計算的babel外掛

實現程式碼如下:

// 預計算簡單表示式的外掛
let code = `const result = 1000 * 60 * 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);
      let parentPath = path.parentPath;
      // 如果此表示式的parent也是一個表示式的話,需要遞迴計算
      if (path.parentPath.node.type == 'BinaryExpression') {
        visitor.BinaryExpression.call(null, path.parentPath)
      }
    }
  }
}

let cal = babel.transform(code, {
  plugins: [
    {visitor}
  ]
});
複製程式碼

參考文章

AST抽象語法樹

一看就懂的JS抽象語法樹

深入Babel,這一篇就夠了

珠峰前端架構師培訓課程

相關文章