本文会将介绍AST抽象语法树的概念和基本原理、AST抽象语法树的遍历和生成、如何使用babel插件进行代码转换以及如何自定义 babel 插件。
实现我们先介绍一下抽象语法树(Abstract Syntax Tree,AST)
什么是抽象语法树(Abstract Syntax Tree,AST)? 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的⼀种抽象表示, 它以树状的形式表现编程语⾔的语法结构,树上的每个节点都表示源代码中的⼀种结构。类似于虚拟 dom,只是虚拟 dom 是描述 dom 结构。

而 Eslint 以及 babel 都是基于抽象语法树来实现的,包括 JSX 语法和 Vue 中的 template 语法都离不开它。
JSX 通过 babel 最终转化成 React.createElement 这种形式JavaScript Parser 是把 JavaScript 源码转化为抽象语法树的解析器 ,常见的 Parser 有 esprima、traceur、acorn、shift 等。
那么 AST 是如何生成的呢?
JS 执行的第一步是读取 JS 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析生成 AST,最后生成机器码执行。
词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的 Token。Token 是一个不可分割的最小单元,例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等)。最终,整个代码将被分割进一个tokens 列表(或者说一维数组)。
语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
这里提提供一个在线工具: AST explorer,利用它可以看到不同的 parser 解析 JS 代码后得到的 AST。
下面这个栗子是使用一个 esprima 这个 Parser 对 function a(){}生成的语法树。

可以看到这个 AST 对这句代码的描述非常详细。接下来我们不借助这个在线工具来实现它。
代码转换的大致流程为:
实现,我们需要按照三个第三方库:esprima、 estraverse、 escodegen
yarn add esprima estraverse escodegen
然后我们对它们进行导入。
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
通过 esprima.parseScript 方法将我们的代码转换成 AST 语法树。
let code = `function a(){}`;
const ast = esprima.parseScript(code);
console.log(ast);
可以看到输出结果与上面生成的 AST 类似。
Script {
type: 'Program',
body: [
FunctionDeclaration {
type: 'FunctionDeclaration',
id: [Identifier],
params: [],
body: [BlockStatement],
generator: false,
expression: false,
async: false
}
],
sourceType: 'script'
}
有了这么一个树结构之后,我们当然希望能够对它进去遍历,这个时候我们需要用到 estraverse.traverse。
estraverse.traverse(ast, {
enter(node) { // Program -> FunctionDeclaration -> Identifier
console.log('enter:' + node.type)
if (node.type === 'FunctionDeclaration') {
node.id.name = 'ast'
}
},
leave(node) {
console.log('leave:' + node.type)
}
});
访问模式就是遍历节点的时候会有两个过程,一个是进入一个是离开,分别对应 enter 和 leave 两个钩子函数,并且我们在其中修改函数的标识符。
enter:Program
enter:FunctionDeclaration
enter:Identifier
leave:Identifier
enter:BlockStatement
leave:BlockStatement
leave:FunctionDeclaration
leave:Program
最后我们再通过 escodegen.generate 方法来生成代码。
console.log(escodegen.generate(ast));
输出结果如下。
function ast() {
}
可以看到函数名已经被修改了,至此我们便实现了一个简单的代码转换。
接下来我们来写个 babel 插件,来感受一下 babel 是如何进行转换的。
Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。
Babel 的三个主要处理步骤分别是:解析(parse),转换(transform),生成(generate)。
解析步骤前面已经讲过了,而转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程同时也是插件将要介入工作的部分。代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
比较典型的就是将 ES6 语法转换为 ES5 语法,下面我们来实现两个简单的栗子:转换箭头函数、类编译为 Function
首先我们先介绍一下需要用到的第三方库。
transform、parse,并实现了插件功能 ,也就是说可以在其中放入对应的 babel 插件,在转换的时候会默认调用它们。接下来我们来看看如何借助 babel-plugin-transform-es2015-arrow-functions 来实现转换箭头函数。
const babel = require("@babel/core");
const types = require("@babel/types");
const transformFunction = require("babel-plugin-transform-es2015-arrow-functions");
const code = `const sum = (a,b)=> a+b`;
// 转化代码,通过 transformFunction 插件
const result = babel.transform(code, {
plugins: [transformFunction],
});
console.log(result.code);
最终结果如下。
const sum = function (a, b) {
return a + b;
};
可以看到我们已经成功将箭头函数转成 function 函数。其实这个 transformFunction 插件的原理跟上面的代码转换类似,下面我们先看看这两个结果的 AST。

通过仔细观察,相信你已经看出之间的不同。庆幸的是 @babel/core 为我们提供了很好的插件拓展方式,下面我们来看看如何实现。
const transformFunction = {
visitor: {
ArrowFunctionExpression(path) { // path就是访问的路径 path -> node
let {
node
} = path;
node.type = 'FunctionExpression';
let body = node.body; // 老节点中的 a+b;
if (!types.isBlockStatement(body)) {
node.body = types.blockStatement([types.returnStatement(body)])
}
}
}
}
当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个**访问者模式(visitor)**的概念。访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。我们在对象里定义了 ArrowFunctionExpression 方法,遇到箭头函数表达式会命中此⽅法,如何利用 @babel/types 构造、变换 AST 节点。
然而箭头函数还有一个问题,就是 this 的指向,我们需要单独对它进行处理。例如我们要将以下代码进行转换。
const sum = ()=> console.log(this)
转换后的结果为:
var _this = this;
const sum = function () {
return console.log(_this);
};
可以看到,我们需要找到上层作用域里的 this ,将它赋值给 _this,从而将函数里的 this 改成了 _thiis 。下面我们先看看如何实现。
const transformFunction = {
visitor: {
ArrowFunctionExpression(path) { // path就是访问的路径 path -> node
let { node } = path;
node.type = 'FunctionExpression';
hoistFunctionEvn(path);
let body = node.body; // 老节点中的 a+b;
if (!types.isBlockStatement(body)) {
node.body = types.blockStatement([types.returnStatement(body)])
}
}
}
}
function getThisPath(path) {
let arr = []
path.traverse({
ThisExpression(path) {
arr.push(path);
}
})
return arr;
}
function hoistFunctionEvn(path) {
// 查找父作用域
const thisEnv = path.findParent((parent) => (parent.isFunction() && !parent.isArrowFunctionExpression()) || parent.isProgram())
const bingingThis = '_this'; // var _this = this;
const thisPaths = getThisPath(path);
// 修改当前路径中的this 变为_this
thisPaths.forEach(path => {
// this -> _this
path.replaceWith(types.identifier(bingingThis))
})
thisEnv.scope.push({
id: types.identifier(bingingThis),
init: types.thisExpression()
})
}
我们通过 path.findParent 方法从一个路径向上遍历语法树,直到满足相应的条件。对于每一个父路径调用 callback 并将其 NodePath 当作参数,当 callback 返回真值时,则将其 NodePath 返回。我们的判断条件是:该节点类型是 function 函数 or 根节点。接着我们便需要修改当前路径中的 this 变为 _this。当然这里用到了很多 API 这里便不再一一解释,读者可自行查阅:Babel 插件手册。
同样的,我们要把以下 ES6 语法的类更改为 Function 的方式。
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
setName(newName) {
this.name = newName;
}
}
// 转换后
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.setName = function () {
this.name = newName;
};
下面我们先看看这两个结果的 AST,以及给出实现方式,这里就不再过多解释其中的 API,读者可自行查阅:Babel 插件手册。

const transformFunction = {
visitor: {
ClassDeclaration(path) {
const {
node
} = path;
const id = node.id;
const methods = node.body.body; // 获取类中的⽅法
const nodes = [];
methods.forEach((method) => {
if (method.kind === "constructor") {
let constructorFunction = types.functionDeclaration(
id,
method.params,
method.body
);
nodes.push(constructorFunction);
} else {
// Person.prototype.getName
const memberExpression = types.memberExpression(
types.memberExpression(id, types.identifier("prototype")),
method.key
);
// function(name){return name}
const functionExpression = types.functionExpression(
null,
method.params,
method.body
);
// 赋值
const assignmentExpression = types.assignmentExpression(
"=",
memberExpression,
functionExpression
);
nodes.push(assignmentExpression);
}
});
// 替换节点
if (node.length === 1) {
path.replaceWith(nodes[0]);
} else {
path.replaceWithMultiple(nodes);
}
},
},
};