手把手帶你入門 AST 抽象語法樹

AST 是什麼

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

AST 有什麼用

AST 運用普遍,好比:java

  • 編輯器的錯誤提示、代碼格式化、代碼高亮、代碼自動補全;
  • elintpretiier 對代碼錯誤或風格的檢查;
  • webpack 經過 babel 轉譯 javascript 語法;

而且若是你想了解 js 編譯執行的原理,那麼你就得了解 AST。node

AST 如何生成

js 執行的第一步是讀取 js 文件中的字符流,而後經過詞法分析生成 token,以後再經過語法分析( Parser )生成 AST,最後生成機器碼執行。webpack

整個解析過程主要分爲如下兩個步驟:git

  • 分詞:將整個代碼字符串分割成最小語法單元數組
  • 語法分析:在分詞基礎上創建分析語法單元之間的關係

JS Parser 是 js 語法解析器,它能夠將 js 源碼轉成 AST,常見的 Parser 有 esprima、traceur、acorn、shift 等。github

詞法分析

詞法分析,也稱之爲掃描(scanner),簡單來講就是調用 next() 方法,一個一個字母的來讀取字符,而後與定義好的 JavaScript 關鍵字符作比較,生成對應的Token。Token 是一個不可分割的最小單元:web

例如 var 這三個字符,它只能做爲一個總體,語義上不能再被分解,所以它是一個 Token。npm

詞法分析器裏,每一個關鍵字是一個 Token ,每一個標識符是一個 Token,每一個操做符是一個 Token,每一個標點符號也都是一個 Token。除此以外,還會過濾掉源程序中的註釋和空白字符(換行符、空格、製表符等。編程

最終,整個代碼將被分割進一個tokens列表(或者說一維數組)。json

語法分析

語法分析會將詞法分析出來的 Token 轉化成有語法含義的抽象語法樹結構。同時,驗證語法,語法若是有錯的話,拋出語法錯誤。

說了這麼多咱們來看下 javaScript 代碼片斷轉成 AST 以後是什麼樣的咱們拿一行簡單的代碼來展現

🌰例子 1

const fn = a => a;
複製代碼

如圖從這個 AST 語法樹咱們就可以很清楚的看出一個代碼他的具體含義,而且使用的是什麼語法,方法等。

用人話翻譯這個圖就是:用類型 const 聲明變量 fn 指向一個箭頭函數表達式,它的參數是 a 函數體也是 a。

🌰例子 2

const fn = a => {
    let i = 1;
  return a + i;
};
複製代碼

咱們來看 body 這塊:

🌰例子 3

函數調用

function test(){
  let a = 1;
  console.log(a)
}
複製代碼

主要看 MemberExpression

以上截圖均是使用 Acorn 解析。使用 Acorn 的緣由是據我瞭解在 parser 解析中,Acorn 是公認的最快的。而且咱們使用的 Webpack 打包工具中 babel 用的也是 Acorn。

上述截圖的屬性是 AST 的一部分,這個結構包含了不少屬性。

  • VariableDeclaration 變量聲明
  • VariableDeclarator 變量聲明的描述
  • Expression 表達式節點

更多屬性展現:

  1. 能夠去 AST explorer 能夠在線看到不一樣的 parser 解析 js 代碼後獲得的 AST。
  2. github 上看全部的 ESTree ESTree
  3. 關於屬性介紹的文檔 抽象語法樹AST介紹

實戰 AST 的運用

題目

經過上面介紹的 console.log AST,下面咱們就來完成一個在調用 console.log(xx) 時候給前面加一個函數名,這樣用戶在打印時候能改方便看到是哪一個函數調用的。

舉例

// 源代碼
function getData() {
  console.log("data")
}

// --------------------

// 轉化後代碼
function getData() {
  console.log("getData", "data");
}
複製代碼

介紹

首先介紹下咱們須要使用的工具 Babel

  • @babel/parser : 將 js 代碼 ------->>> AST 抽象語法樹;
  • @babel/traverseAST 節點進行遞歸遍歷;
  • @babel/types 對具體的 AST 節點進行進行修改;
  • @babel/generator : AST 抽象語法樹 ------->>> 新的 js 代碼;

爲何使用 babel ? 主要是比較好用(只對這個比較熟悉😭)。

進入 @babel/parser 官網開頭就介紹了它是使用的 Acorn 來解析 js 代碼成 AST 語法樹(說明確實 Acorn 比較好)。

開始碼起來

  1. 新建文件打開控制檯安裝須要的包
cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D
複製代碼
  1. 建立 js 文件, 編寫大體佈局以下 使用 AST
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function compile(code) {
  // 1.parse 將代碼解析爲抽象語法樹(AST)
  const ast = parser.parse(code);

  // 2,traverse 轉換代碼
  traverse.default(ast, {});

  // 3. generator 將 AST 轉回成代碼
  return generator.default(ast, {}, code);
}

const code = ` function getData() { console.log("data") } `;
const newCode = compile(code)
複製代碼

使用 node 跑出結果,由於什麼都沒處理,輸出的是原代碼,

完善 compile 方法

function compile(code) {
  // 1.parse
  const ast = parser.parse(code);

  // 2,traverse
  const visitor = {
    CallExpression(path) {
      // 拿到 callee 數據
      const { callee } = path.node;
      // 判斷是不是調用了 console.log 方法
      // 1. 判斷是不是成員表達式節點,上面截圖有詳細介紹
      // 2. 判斷是不是 console 對象
      // 3. 判斷對象的屬性是不是 log
      const isConsoleLog =
        types.isMemberExpression(callee) &&
        callee.object.name === "console" &&
        callee.property.name === "log";
      if (isConsoleLog) {
        // 若是是 console.log 的調用 找到上一個父節點是函數
        const funcPath = path.findParent(p => {
          return p.isFunctionDeclaration();
        });
        // 取函數的名稱
        const funcName = funcPath.node.id.name;
        // 將名稱經過 types 來放到函數的參數前面去
        path.node.arguments.unshift(types.stringLiteral(funcName));
      }
    }
  };
  // traverse 轉換代碼
  traverse.default(ast, visitor);

  // 3. generator 將 AST 轉回成代碼
  return generator.default(ast, {}, code);
}
複製代碼

純代碼看起來比較難理解下面是我將上面的 path.node 寫入到文件中給你們看下數據格式。

{
  "type": "CallExpression",
  "start": 24,
  "end": 43,
  "loc": {
    "start": { "line": 3, "column": 2 },
    "end": { "line": 3, "column": 21 }
  },
  "callee": {
    "type": "MemberExpression",
    "start": 24,
    "end": 35,
    "loc": {
      "start": { "line": 3, "column": 2 },
      "end": { "line": 3, "column": 13 }
    },
    "object": {
      "type": "Identifier",
      "start": 24,
      "end": 31,
      "loc": {
        "start": { "line": 3, "column": 2 },
        "end": { "line": 3, "column": 9 },
        "identifierName": "console"
      },
      "name": "console"
    },
    "property": {
      "type": "Identifier",
      "start": 32,
      "end": 35,
      "loc": {
        "start": { "line": 3, "column": 10 },
        "end": { "line": 3, "column": 13 },
        "identifierName": "log"
      },
      "name": "log"
    },
    "computed": false
  },
  "arguments": [
    {
      "type": "StringLiteral",
      "start": 36,
      "end": 42,
      "loc": {
        "start": { "line": 3, "column": 14 },
        "end": { "line": 3, "column": 20 }
      },
      "extra": { "rawValue": "data", "raw": "'data'" },
      "value": "data"
    }
  ]
}

複製代碼

咱們將沒必要要的位置信息(start, end, loc)屬性刪除,對照數據來看代碼將會一目瞭然

再跑該文件

很好,調用 console.log 方法參數前面增長了函數名,完成!!

爲了你們可以方便運行,下面是完整代碼

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");


function compile(code) {
  // 1.parse
  const ast = parser.parse(code);

  // 2,traverse
  const visitor = {
    CallExpression(path) {
      const { callee } = path.node;
      const isConsoleLog =
        types.isMemberExpression(callee) &&
        callee.object.name === "console" &&
        callee.property.name === "log";
      if (isConsoleLog) {
        const funcPath = path.findParent(p => {
          return p.isFunctionDeclaration();
        });
        const funcName = funcPath.node.id.name;
        fs.writeFileSync("./funcPath.json", JSON.stringify(funcPath.node), err => {
          if (err) throw err;
          console.log("寫入成功");
        });
        path.node.arguments.unshift(types.stringLiteral(funcName));
      }
    }
  };
  traverse.default(ast, visitor);

  // 3. generator
  return generator.default(ast, {}, code);
}

const code = ` function getData() { console.log('data') } `;
console.log(compile(code).code);

複製代碼

看到這裏,若是你以爲都沒什麼問題,相信你對 AST 已經有了很清楚的認識了,而且對 babel 編譯代碼也有了必定的理解,之後寫 webpack 配置也就不會對 babel 那麼陌生了。

總結

爲了兼容低版本瀏覽器 咱們也一般會使用 webpack 打包編譯咱們的代碼將 ES6 語法下降版本,好比箭頭函數變成普通函數。將 const、let 聲明改爲 var 等等,他都是經過 AST 來完成的,只不過實現的過程比較複雜,精緻。不過也都是這三板斧:

  1. js 語法解析成 AST;
  2. 修改 AST;
  3. AST 轉成 js 語法;

最後

有時間,你們在嘗試完成以後也一樣能夠試試箭頭函數轉普通函數等一些經常使用的代碼轉換,這樣能夠很好的加深印象。

全文章,若有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

參考

相關文章
相關標籤/搜索