AST 抽象語法樹

在這裏插入圖片描述


原文出自:https://www.pandashen.com


AST 抽象語法樹簡介

AST(Abstract Syntax Tree)是源代碼的抽象語法結構樹狀表現形式,Webpack、ESLint、JSX、TypeScript 的編譯和模塊化規則之間的轉化都是通過 AST 來實現對代碼的檢查、分析以及編譯等操作。


JavaScript 語法的 AST 語法樹

JavaScript 中想要使用 AST 進行開發,要知道抽象成語法樹之後的結構是什麼,裏面的字段名稱都代表什麼含義以及遍歷的規則,可以通過 http://esprima.org/demo/parse... 來實現 JavaScript 語法的在線轉換。

通過在線編譯工具,可以將 function fn(a, b) {} 編譯爲下面的結構。

{
    "type": "Program",
    "body": [
        {
            "type": "FunctionDeclaration",
            "id": {
                "type": "Identifier",
                "name": "fn"
            },
            "params": [
                {
                    "type": "Identifier",
                    "name": "a"
                },
                {
                    "type": "Identifier",
                    "name": "b"
                }
            ],
            "body": {
                "type": "BlockStatement",
                "body": []
            },
            "generator": false,
            "expression": false,
            "async": false
        }
    ],
    "sourceType": "script"
}

將 JavaScript 語法編譯成抽象語法樹後,需要對它進行遍歷、修該並重新編譯,遍歷樹結構的過程爲 “先序深度優先”。


esprima、estraverse 和 escodegen

esprimaestraverseescodegen 模塊是操作 AST 的三個重要模塊,也是實現 babel 的核心依賴,下面是分別介紹三個模塊的作用。

1、esprima 將 JS 轉換成 AST

esprima 模塊的用法如下:

// 文件:esprima-test.js
const esprima = require("esprima");

let code = "function fn() {}";

// 生成語法樹
let tree = esprima.parseScript(code);

console.log(tree);

// Script {
//   type: 'Program',
//   body:
//    [ FunctionDeclaration {
//        type: 'FunctionDeclaration',
//        id: [Identifier],
//        params: [],
//        body: [BlockStatement],
//        generator: false,
//        expression: false,
//        async: false } ],
//   sourceType: 'script' }

通過上面的案例可以看出,通過 esprima 模塊的 parseScript 方法將 JS 代碼塊轉換成語法樹,代碼塊需要轉換成字符串,也可以通過 parseModule 方法轉換一個模塊。

2、estraverse 遍歷和修改 AST

查看遍歷過程:

// 文件:estraverse-test.js
const esprima = require("esprima");
const estraverse = require("estraverse");

let code = "function fn() {}";

// 遍歷語法樹
estraverse.traverse(esprima.parseScript(code), {
    enter(node) {
        console.log("enter", node.type);
    },
    leave() {
        console.log("leave", node.type);
    }
});

// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// leave Program

上面代碼通過 estraverse 模塊的 traverse 方法將 esprima 模塊轉換的 AST 進行了遍歷,並打印了所有的 type 屬性並打印,每含有一個 type 屬性的對象被叫做一個節點,修改是獲取對應的類型並修改該節點中的屬性即可。

其實深度遍歷 AST 就是在遍歷每一層的 type 屬性,所以遍歷會分爲兩個階段,進入階段和離開階段,在 estraversetraverse 方法中分別用參數指定的 entryleave 兩個函數監聽,但是我們一般只使用 entry

3、escodegen 將 AST 轉換成 JS

下面的案例是一個段 JS 代碼塊被轉換成 AST,並將遍歷、修改後的 AST 重新轉換成 JS 的全過程。

// 文件:escodegen-test.js
const esprima = require("esprima");
const estraverse = require("estraverse");
const escodegen = require("escodegen");

let code = "function fn() {}";

// 生成語法樹
let tree = esprima.parseScript(code);

// 遍歷語法樹
estraverse.traverse(tree, {
    enter(node) {
        // 修改函數名
        if (node.type === "FunctionDeclaration") {
            node.id.name = "ast";
        }
    }
});

// 編譯語法樹
let result = escodegen.generate(tree);

console.log(result);

// function ast() {
// }

在遍歷 AST 的過程中 params 值爲數組,沒有 type 屬性。


實現 Babel 語法轉換插件

實現語法轉換插件需要藉助 babel-corebabel-types 兩個模塊,其實這兩個模塊就是依賴 esprimaestraverseescodegen 的。

使用這兩個模塊需要安裝,命令如下:

npm install babel-core babel-types

1、plugin-transform-arrow-functions

plugin-transform-arrow-functions 是 Babel 家族成員之一,用於將箭頭函數轉換 ES5 語法的函數表達式。

// 文件:plugin-transform-arrow-functions.js
const babel = require("babel-core");
const types = require("babel-types");

// 箭頭函數代碼塊
let sumCode = `
const sum = (a, b) => {
    return a + b;
}`;
let minusCode = `const minus = (a, b) => a - b;`;

// 轉化 ES5 插件
let ArrowPlugin = {
    // 訪問者(訪問者模式)
    visitor: {
        // path 是樹的路徑
        ArrowFunctionExpression(path) {
            // 獲取樹節點
            let node = path.node;

            // 獲取參數和函數體
            let params = node.params;
            let body = node.body;

            // 判斷函數體是否是代碼塊,不是代碼塊則添加 return 和 {}
            if (!types.isBlockStatement(body)) {
                let returnStatement = types.returnStatement(body);
                body = types.blockStatement([returnStatement]);
            }

            // 生成一個函數表達式樹結構
            let func = types.functionExpression(null, params, body, false, false);

            // 用新的樹結構替換掉舊的樹結構
            types.replaceWith(func);
        }
    }
};

// 生成轉換後的代碼塊
let sumResult = babel.transform(sumCode, {
    plugins: [ArrowPlugin]
});

let minusResult = babel.transform(minusCode, {
    plugins: [ArrowPlugin]
});

console.log(sumResult.code);
console.log(minusResult.code);

// let sum = function (a, b) {
//   return a + b;
// };
// let minus = function (a, b) {
//   return a - b;
// };

我們主要使用 babel-coretransform 方法將 AST 轉化成代碼塊,第一個參數爲轉換前的代碼塊(字符串),第二個參數爲配置項,其中 plugins 值爲數組,存儲修改 babal-core 轉換的 AST 的插件(對象),使用 transform 方法將舊的 AST 處理成新的代碼塊後,返回值爲一個對象,對象的 code 屬性爲轉換後的代碼塊(字符串)。

內部修改通過 babel-types 模塊提供的方法實現,API 可以到 https://github.com/babel/babe... 中查看。

ArrowPlugin 就是傳入 transform 方法的插件,必須含有 visitor 屬性(固定),值同爲對象,用於存儲修改語法樹的方法,方法名要嚴格按照 API,對應的方法會修改 AST 對應的節點。

types.functionExpression 方法中參數分別代表,函數名(匿名函數爲 null)、函數參數(必填)、函數體(必填)、是否爲 generator 函數(默認 false)、是否爲 async 函數(默認 false),返回值爲修改後的 AST,types.replaceWith 方法用於替換 AST,參數爲新的 AST。

2、plugin-transform-classes

plugin-transform-classes 也是 Babel 家族中的成員之一,用於將 ES6 的 class 類轉換成 ES5 的構造函數。

// 文件:plugin-transform-classes.js
const babel = require("babel-core");
const types = require("babel-types");

// 類
let code = `
class Person {
    constructor(name) {
        this.name = name;
    }
    getName () {
        return this.name;
    }
}`;

// 將類轉化 ES5 構造函數插件
let ClassPlugin = {
    visitor: {
        ClassDeclaration(path) {
            let node = path.node;
            let classList = node.body.body;

            // 將取到的類名轉換成標識符 { type: 'Identifier', name: 'Person' }
            let className = types.identifier(node.id.name);
            let body = types.blockStatement([]);
            let func = types.functionDeclaration(className, [], body, false, false);
            path.replaceWith(func);

            // 用於存儲多個原型方法
            let es5Func = [];

            // 獲取 class 中的代碼體
            classList.forEach((item, index) => {
                // 函數的代碼體
                let body = classList[index].body;

                // 獲取參數
                let params = item.params.length ? item.params.map(val => val.name) : [];

                // 轉化參數爲標識符
                params = types.identifier(params);

                // 判斷是否是 constructor,如果構造函數那就生成新的函數替換
                if (item.kind === "constructor") {
                    // 生成一個構造函數樹結構
                    func = types.functionDeclaration(className, [params], body, false, false);
                } else {
                    // 其他情況是原型方法
                    let proto = types.memberExpression(className, types.identifier("prototype"));

                    // 左側層層定義標識符 Person.prototype.getName
                    let left = types.memberExpression(proto, types.identifier(item.key.name));

                    // 右側定義匿名函數
                    let right = types.functionExpression(null, [params], body, false, false);

                    // 將左側和右側進行合併並存入數組
                    es5Func.push(types.assignmentExpression("=", left, right));
                }
            });

            // 如果沒有原型方法,直接替換
            if (es5Func.length === 0) {
                path.replaceWith(func);
            } else {
                es5Func.push(func);
                // 替換 n 個節點
                path.replaceWithMultiple(es5Func);
            }
        }
    }
};

// 生成轉換後的代碼塊
result = babel.transform(code, {
    plugins: [ClassPlugin]
});

console.log(result.code);

// Person.prototype.getName = function () {
//     return this.name;
// }
// function Person(name) {
//     this.name = name;
// }

上面這個插件的實現要比 plugin-transform-arrow-functions 複雜一些,歸根結底還是將要互相轉換的 ES6 和 ES5 語法樹做對比,找到他們的不同,並使用 babel-types 提供的 API 對語法樹對應的節點屬性進行修改並替換語法樹,值得注意的是 path.replaceWithMultiplepath.replaceWith 不同,參數爲一個數組,數組支持多個語法樹結構,可根據具體修改語法樹的場景選擇使用,也可根據不同情況使用不同的替換方法。


總結

通過本節我們瞭解了什麼是 AST 抽象語法樹、抽象語法樹在 JavaScript 中的體現以及在 NodeJS 中用於生成、遍歷和修改 AST 抽象語法樹的核心依賴,並通過使用 babel-corebabel-types 兩個模塊簡易模擬了 ES6 新特性轉換爲 ES5 語法的過程,希望可以爲後面自己實現一些編譯插件提供了思路。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章