本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-07-03
『转载』
抽象语法树即:Abstract Syntax Tree。简称AST。webpack和Lint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的。通过了解抽象语法树这个概念,你也可以随手编写类似的工具。
抽象语法树生成的过程为:代码 => 词法分析 => 语法分析 => AST,如下图,我们看看一个简单函数声明的构成
通过 https://astexplorer.net/ 可以在线解析出很多程序语言的 ast。
function square(n) {
return n * n;
}
代码解析后的AST 对象如下:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
js引擎在执行js文件时,都会先将js代码转换成抽象语法树(AST)。一位Mozilla工程师在FireFox中公开了这个将代码转成AST的解析器Api,也就是Parser_API,后来被人整理到github项目estree,慢慢的成了业界的规范。
下面介绍几个AST里的几个基本元素:
1、Node objects
interface Node {
type: string; //type 字段是一个字符串,代表AST变量类型,你可以使用这个字段去决定一个节点要实现的接口.
loc: SourceLocation | null; // loc 字段代表节点的源位置信息
}
// 代码位置信息
interface SourceLocation {
source: string | null;
start: Position;
end: Position;
}
// 位置信息
interface Position {
line: uint32 >= 1;
column: uint32 >= 0;
}
2、Programs(程序语句)
// A complete program source tree.
interface Program <: Node {
type: "Program";
body: [ Statement ];
}
3、Identifier(标识符,我们写代码时自定义的名称,如变量名、函数名、属性名)
// A complete program source tree.
interface Identifier <: Node, Expression, Pattern {
type: "Identifier";
name: string;
}
4、Literal(字面量,“hello”、 true、 null、 100、 /\d/)
interface Literal <: Node, Expression {
type: "Literal";
value: string | boolean | null | number | RegExp;
}
5、Functions(函数)
interface Function <: Node {
id: Identifier | null;
params: [ Pattern ];
defaults: [ Expression ];
rest: Identifier | null;
body: BlockStatement | Expression;
generator: boolean;
expression: boolean;
}
6、Statements(语句,子类有很多,空语句,if、switch 语句等)
interface Statement <: Node { }
// 一个空语句,也就是,一个孤立的分号
interface EmptyStatement <: Statement {
type: "EmptyStatement";
}
// 一个语句块,也就是由大括号包围的语句序列.
interface BlockStatement <: Statement {
type: "BlockStatement";
body: [ Statement ];
}
// 表达式语句,也就是,仅有一个表达式组成的语句.
interface ExpressionStatement <: Statement {
type: "ExpressionStatement";
expression: Expression;
}
7、Declarations(声明)
// 声明,子类主要有变量申明、函数声明。
interface Declaration <: Statement { }
8、Expressions(表达式)
// 表达式,子类很多,有二元表达式( n*n)
// 函数表达式(var fun = function(){})、数组表达式(var arr = [])
// 对象表达式(var obj = {})、赋值表达式( a=1)等等
// this 表达式
interface ThisExpression <: Expression {
type: "ThisExpression";
}
// 数组表达式
interface ArrayExpression <: Expression {
type: "ArrayExpression";
elements: [ Expression | null ];
}
//对象表达式
interface ObjectExpression <: Expression {
type: "ObjectExpression";
properties: [
{
key: Literal | Identifier,
value: Expression,
kind: "init" | "get" | "set"
}
];
}
9、Patterns(模式)
模式,主要在 ES6 的解构赋值中有意义,let {name} = user
,其中{name}
部分为 ObjectPattern,在 ES5 中,可以理解为和 Identifier 差不多的东西。
业界已经有很多成熟的解析器,可以将js代码转换成符合规范的AST:
我们选用 babel 的解析器来实践一下:
babel 主要有四个包来处理ast ,@babel/parser
用来解析代码为AST,@babel/traverse
用来转换代码时遍历代码,@babel/generator
转换AST为code ,@babel/types
是上述AST 的类型定义及一些工具函数,类似于lodash,方便我们操作节点的工具函数
1、用 ast 逆向构造一个构造代码块 const add = (a ,b)=>{return a + b;}
const parse = require('@babel/parser').parse; //解析代码块
const traverse = require('@babel/traverse').default; //遍历代码块
const generate = require('@babel/generator').default; // 生成代码块
const t = require('@babel/types'); // 工具函数
console.log(
generate(
t.variableDeclaration('const',[ //变量声明
t.variableDeclarator(//变量定义
t.identifier('add'),//标识符定义
t.arrowFunctionExpression( //剪头函数表达式
[],
t.blockStatement(// 语句块 {}
[
t.returnStatement(// return 语句块
t.binaryExpression( //二元表达式
"+",
t.identifier('a'),
t.identifier('b')
)
),
]
)
)
)
])
).code
)
2、去除代码中的 console.log
和 debugger
语句
const parse = require('@babel/parser').parse; //解析代码块
const traverse = require('@babel/traverse').default; //遍历代码块
const generate = require('@babel/generator').default; // 生成代码块
const t = require('@babel/types'); // 工具函数
const code = `
function add (a,b) {
// 嗯,去除 console.log 和 debugger
console.log('xxxx');
debugger;
return a + b;
}
`
const ast = parse(code);
traverse(ast, {
// debugger语句
DebuggerStatement(path){
path.remove();
},
// 调用表达式
CallExpression(path){
const {callee} = path.node;
if(callee.type = 'MemberExpression' && callee.object.name === 'console' && callee.property.name ==='log'){
path.remove();
}
}
});
console.log(generate(ast,{comments:false}).code);
3、导出文件的所有函数
const parse = require('@babel/parser').parse; //解析代码块
const traverse = require('@babel/traverse').default; //遍历代码块
const generate = require('@babel/generator').default; // 生成代码块
const t = require('@babel/types'); // 工具函数
const code = `
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
function commonDivision(a, b) {
while(b !== 0){
if(a > b){
a = sub(a, b);
}else{
b = sub(b, a)
}
}
return a;
}`;
// 完全遍历替换代码中的function
let functionIds = [];
const ast = parse(code);
traverse(ast,{
FunctionDeclaration(path){
const node = path.node;
const funcName = node.id;
const params = node.params;
const body = node.body;
functionIds.push(funcName.name);
const rep = t.expressionStatement(
t.assignmentExpression( //赋值表达式
'=',
t.memberExpression( //成员表达式
t.identifier('exports'),
funcName
),
t.arrowFunctionExpression(params,body) //箭头函数表达式
)
)
path.replaceWith(rep);
}
})
// 遍历调用改为 exports.xxxx
traverse(ast,{
CallExpression(path){
const node = path.node;
if(functionIds.includes(node.callee.name)){
node.callee = t.memberExpression(t.identifier('exports'),node.callee);
}
}
})
console.log(generate(ast).code);
4、编写 一个babel插件,删除文件中的 debugger
// babel 配置
// babel.config.js
const removePlugin = require('./babel-plugin-debugger-remove-plugin');
// 预设
const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
},
],
];
// 添加 自定义插件
const plugins = [
[removePlugin,{isOpen:true}]
];
module.exports = { presets, plugins };
// babel-plugin-debugger-remove-plugin.js
// babel debugger去除插件
module.exports = function({ types: t }) {
return {
visitor: {
DebuggerStatement(path, state= {opts:{isOpen:true}}) {
console.log(state.opts.isOpen);
if(state.opts.isOpen){
path.remove();
}
}
}
};
};
几个例子下来我们可以了解到,ast可以让我们更加了解javascript.也可以开发一些工具来优化转换代码。平常开发过程中,很少接触AST,但包括Vue/react,等的模板转换都以AST为基础。以上抛砖引玉介绍一下简单例子,希望同学们在研究AST过程中可以产出更多基于AST的优秀工具、项目。