java抽象语法树(ast),AST 抽象语法树

java抽象语法树(ast),AST 抽象语法树AST 抽象语法树简介 AST AbstractSynt 是源代码的抽象语法结构树状表现形式 Webpack ESLint JSX TypeScript 的编译和模块化规则之间的转化都是通过 AST 来实现对代码的检查 分析以及编译等操作 JavaScript 语法的 AST 语法树 JavaScript 中想要使用 AST 进行开发 要知道抽象成语法树之后的结构是什么 里面的字段名称

1460000018997898

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

esprima、estraverse 和 escodegen 模块是操作 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 属性,所以遍历会分为两个阶段,进入阶段和离开阶段,在 estraverse 的 traverse 方法中分别用参数指定的 entry 和 leave 两个函数监听,但是我们一般只使用 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-core 和 babel-types 两个模块,其实这两个模块就是依赖 esprima、estraverse 和 escodegen 的。

使用这两个模块需要安装,命令如下:

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-core 的 transform 方法将 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.replaceWithMultiple 与 path.replaceWith 不同,参数为一个数组,数组支持多个语法树结构,可根据具体修改语法树的场景选择使用,也可根据不同情况使用不同的替换方法。

总结

通过本节我们了解了什么是 AST 抽象语法树、抽象语法树在 JavaScript 中的体现以及在 NodeJS 中用于生成、遍历和修改 AST 抽象语法树的核心依赖,并通过使用 babel-core 和 babel-types 两个模块简易模拟了 ES6 新特性转换为 ES5 语法的过程,希望可以为后面自己实现一些编译插件提供了思路。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/218902.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 下午11:12
下一篇 2026年3月17日 下午11:12


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号