浅析AST
什么是Ast(Abstract Syntax Tree)
抽象语法树(Abstract Syntax Tree)简称是AST。顾名思义就是将源代码的语法结构抽象成树状表现形式,树上的每一个节点都对应着源码中的一部分。所谓的"树"就是把我们的js语句转化为一个json对象,有根节点、分枝、树叶像是一棵完整的树。
我们可以通过astexplorer从线上观察AST的转化结果。
那为什么要把我们看起来好懂的js代码,转化为一个抽象的树呢?
其实对于javaScript引擎来说,做的第一件事就是把我们的js代码转化为抽象语法树,之后才是转换为字节码供解释器使用。虽然对于我们程序员来说,js代码更容易理解,但是对于计算机来说,这个抽象的树才是更容易理解的。
AST的用途
AST的使用其实还是相当广泛的,即使你没有听说过AST,但是babel``eslint``prettier
你肯定是接触过的,而这些工具,他们对静态代码进行翻译、代码检查、格式化等等功能实现都是通过AST。
AST解析流程
AST是深度优先的遍历,常见的jsParser有estree、esprima、acorn、babel parser,这里我们通过esprima简单的了解一下流程。
安装如下npm包
npm i esprima estraverse escodegen --save
- esprima:将JS源代码转化为AST语法树。
- estraverse:遍历语法树,修改树上的节点。
- escodegen:把AST语法树重新转化为代码。
const esprima = require('esprima'); //把JS源代码转成AST语法树
const estraverse = require('estraverse'); ///遍历语法树,修改树上的节点
let escodegen = require('escodegen'); //把AST语法树重新转换成代码
const code = `function ast(){}`;
const ast = esprima.parse(code);
estraverse.traverse(ast, {
enter(node) {
console.log(node.type + '进入');
if (node.type === 'FunctionDeclaration') {
node.id.name = 'newAst';
}
},
leave(node) {
console.log(node.type + '离开');
},
});
const newCode = escodegen.generate(ast);
console.log(newCode);
输出结果如下:
Program进入
FunctionDeclaration进入
Identifier进入
Identifier离开
BlockStatement进入
BlockStatement离开
FunctionDeclaration离开
Program离开
function newAst() {
}
我们成功的生成了code
的语法树,成功遍历并修改函数名称,将ast => newAst。
babel中的AST
我们都知道babel可以将Es6代码转换为Es5代码,来解决一些浏览器不兼容Es6新特性的问题,babel能完成这项工作自然是离不开AST。
工作过程分为三个部分:
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
- Transform(转换) 对抽象语法树进行转换
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
实现一个babel插件
visitor
- 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
- Visitor 的对象定义了用于 AST 中获取具体节点的方法
- Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法
体验官方插件
在这里实现一个箭头函数转换为普通函数的插件,这里我选择使用的是babel中@babel/core和@babel/types实现,这里的babel-plugin-transform-es2015-arrow-functions是官方插件,代码如下:
npm install @babel/core @babel/core @babel/types babel-plugin-transform-es2015-arrow-functions --save-dev
const core = require('@babel/core');
const types = require('@babel/types');
const arrowFunctionPlugin = require('babel-plugin-transform-es2015-arrow-functions');
const sourceCode = `
const sum = (a, b) => {
return a+b;
}
`;
const result = core.transform(sourceCode, {
plugins: [arrowFunctionPlugin],
});
console.log(result);
// 输出结果如下
/*
const sum = function (a, b) {
return a + b;
};
*/
实现插件
下面我们实现我们自己的插件
首先我们通过astexplorer观察一下箭头函数和普通函数在ast上表现的不同:
看起来只是有图中圈中的位置不同我们尝试改变一下:
const core = require('@babel/core');
const types = require('@babel/types');
const arrowFunctionPlugin = {
visitor: {
ArrowFunctionExpression(path) {
const { node } = path;
node.type = 'FunctionExpression';
},
},
};
const sourceCode = `
const sum = (a, b) => {
return a+b;
}
`;
const result = core.transform(sourceCode, {
plugins: [arrowFunctionPlugin],
});
console.log(result.code);
// 输出结果
/*
const sum = function (a, b) {
return a + b;
};
*/
amazing!🎉🎉🎉 看起来我们已经完成了这个插件,也不过如此。事实上这只是完成了其中的一部分,我们还有一些情况是没有考虑的,这些情况这个插件就会显得能力不足,比如:
// 简写return的情况
const sourceCode = `
const sum = (a, b) => a+b;
`;
// 转化为
const sum = function (a, b) a + b;
而这显然是一个错误的答案,这时我们需要对我们插件进行一些改造。
对比简写和非简写ast的异同
根据对比我们可以发现,非简写比简写的多出来一层BlockStatement节点,也就是我们的{}
,我们可以根据这个不同来丰富我们的插件
const arrowFunctionPlugin = {
visitor: {
ArrowFunctionExpression(path) {
const { node } = path;
node.type = 'FunctionExpression';
const body = node.body;
// 判断是不是一个blockStatement,这里是兼容简写的模式
if (!types.isBlockStatement(body)) {
// 快速方便的构建节点
node.body = types.blockStatement([types.returnStatement(body)]);
}
},
},
};
通过丰富的插件我们可以得到转换为:
// 简写return的情况
const sourceCode = `
const sum = (a, b) => a+b;
`;
// 转化为
const sum = function (a, b) {
return a + b;
};
看来我们距离成功又近了一步。为什么这么说,是因为我们还有最为关键的一步没有实现,就是箭头函数中this指向的问题。我们都知道,箭头函数没有自己的this,他所依赖的是他外层的this。
首先我们看一下,官方插件是怎么处理的。
const sourceCode = `
const sum = (a, b) => {
console.log(this)
return a+b;
}
`;
// 转化为
var _this = this;
const sum = function (a, b) {
cconsole.log(_this);
return a + b;
};
它先在函数外层创建了一个_this
变量,并把当前上下文中的this
赋值给它,之后在函数中引用我们的_this变量即可,这样我们也仿造这么写一个。
- 首先,我们检测在箭头函数中有没有使用到this,
function getThisPaths(path) {
const thisPaths = [];
// 遍历传进来的函数,通过traverse方法将使用到this的节点保存
path.traverse({
ThisExpression(thisPath) {
thisPaths.push(thisPath);
},
});
return thisPaths;
}
- 如果存在使用this的节点,即
thisPaths
的节点的长度是大于0的,就寻找父节点,找到真正的this,这里要保证父节点不能是箭头函数,或者到了根节点就停止。
// 1. 检测当前节点有没有用到this
const thisPaths = getThisPaths(path);
if (thisPaths.length > 0) {
// 2. 寻找外层this的路径,这里要寻找父节点,直到外层是函数且不是箭头函数,或者到了根节点,来确定this的路径
const thisEnv = path.findParent((parent) => {
// 如果是函数,但是不是箭头函数,或者是根节点,就返回true
return (
(parent.isFunction() && !parent.isArrowFunctionExpress()) ||
parent.isProgram()
);
});
}
- 最后一步,创建
_this
变量,保证该路径没有添加过该变量之后,添加该变量,避免重复添加。
const thisBindings = '_this';
// 3. 如果路径中的作用域中没有绑定过_this,就向作用域中增加一个,避免重复绑定
if (!thisEnv.scope.hasBinding(thisBindings)) {
// 向对应的作用域中添加一个_this,他的值就是this
const thisIdentifier = types.identifier(thisBindings);
thisEnv.scope.push({
id: thisIdentifier,
init: types.thisExpression(),
});
// 遍历添加
thisPaths.forEach((thisPath) => {
thisPath.replaceWith(thisIdentifier);
});
}
封装成一个方法
function hoistFunctionEnvironment(path) {
// 1. 检测当前节点有没有用到this
const thisPaths = getThisPaths(path);
if (thisPaths.length > 0) {
// 2. 寻找外层this的路径,这里要寻找父节点,直到外层是函数且不是箭头函数,或者到了根节点,来确定this的路径
const thisEnv = path.findParent((parent) => {
// 如果是函数,但是不是箭头函数,或者是根节点,就返回true
return (
(parent.isFunction() && !parent.isArrowFunctionExpress()) ||
parent.isProgram()
);
});
const thisBindings = '_this';
// 3. 如果路径中的作用域中没有绑定过_this,就向作用域中增加一个,避免重复绑定
if (!thisEnv.scope.hasBinding(thisBindings)) {
// 向对应的作用域中添加一个_this,他的值就是this
const thisIdentifier = types.identifier(thisBindings);
thisEnv.scope.push({
id: thisIdentifier,
init: types.thisExpression(),
});
// 遍历添加
thisPaths.forEach((thisPath) => {
thisPath.replaceWith(thisIdentifier);
});
}
}
}
讲上述函数插入到合适的位置
const arrowFunctionPlugin = {
visitor: {
ArrowFunctionExpression(path) {
const { node } = path;
node.type = 'FunctionExpression';
// 判断this
hoistFunctionEnvironment(path);
const body = node.body;
// 判断是不是一个blockStatement,这里是想兼容简写的模式
if (!types.isBlockStatement(body)) {
// 快速方便的构建节点
node.body = types.blockStatement([types.returnStatement(body)]);
}
},
},
};
到这里我们的插件就基本上算是完成了,体验一下
// 转化前
const sourceCode = `
const sum = (a, b) => {
cconsole.log(this)
return a+b;
}
`;
// 转化后
var _this = this;
const sum = function (a, b) {
cconsole.log(_this);
return a + b;
};
🎉🎉🎉 这就与官方插件一致了。
完整代码
const core = require('@babel/core');
const types = require('@babel/types');
const arrowFunctionPlugin = {
visitor: {
ArrowFunctionExpression(path) {
const { node } = path;
node.type = 'FunctionExpression';
// 判断this
hoistFunctionEnvironment(path);
const body = node.body;
// 判断是不是一个blockStatement,这里是想兼容简写的模式
if (!types.isBlockStatement(body)) {
// 快速方便的构建节点
node.body = types.blockStatement([types.returnStatement(body)]);
}
},
},
};
function hoistFunctionEnvironment(path) {
// 1. 检测当前节点有没有用到this
const thisPaths = getThisPaths(path);
if (thisPaths.length > 0) {
// 2. 寻找外层this的路径,这里要寻找父节点,直到外层是函数且不是箭头函数,或者到了根节点,来确定this的路径
const thisEnv = path.findParent((parent) => {
// 如果是函数,但是不是箭头函数,或者是根节点,就返回true
return (
(parent.isFunction() && !parent.isArrowFunctionExpress()) ||
parent.isProgram()
);
});
const thisBindings = '_this';
// 3. 如果路径中的作用域中没有绑定过_this,就向作用域中增加一个,避免重复绑定
if (!thisEnv.scope.hasBinding(thisBindings)) {
// 向对应的作用域中添加一个_this,他的值就是this
const thisIdentifier = types.identifier(thisBindings);
thisEnv.scope.push({
id: thisIdentifier,
init: types.thisExpression(),
});
// 遍历添加
thisPaths.forEach((thisPath) => {
thisPath.replaceWith(thisIdentifier);
});
}
}
}
function getThisPaths(path) {
const thisPaths = [];
path.traverse({
ThisExpression(thisPath) {
thisPaths.push(thisPath);
},
});
return thisPaths;
}
const sourceCode = `
const sum = (a, b) => {
cconsole.log(this)
return a+b;
}
`;
const result = core.transform(sourceCode, {
plugins: [arrowFunctionPlugin],
});
console.log(result.code);
转载自:https://juejin.cn/post/7179577515663589435