概述
本文主要分 3 个部分
第一部分:介绍什么是 AST,以及我们为什么要用到它
第二部分:实现一个小型的编译器,理解 AST 在其中起到的作用
第三部分:介绍 AST 在 babel 中是怎么应用的,加深印象
其中第二部分最重要,建议下载示例代码,体会一下整个流程
babel 在前端开发者中是耳熟能详的,它的作用是将ES6、ES7或在草案中的语法转换为ES5语法,
这样在浏览器中没有兼容问题,开发者可以用尝试最新的语法,
你有没有这样的疑问,babel是以什么样的原理实现的这种能力呢?
………………
思考5s
…. done ….
babel实现这种转换,其实是基于一种 AST(抽象语法树)的技术,后面都简称为AST
基于AST不止能实现ES6转换到ES5,下面这些都是目前的应用
- 语法校验(eslint)
- 代码格式化
- css预处理
- 编程语言转换(例 Lisp to JavaScript)
- 一套代码跨端实现方案(React Native、Taro、uni-app等)
在整个前端工程化体系里,都有涉及,但这并不是全部。
对于特定类型的代码文件,你可以用它(AST)来做任何事
什么是AST?
我们先来看一下定义
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code. // 翻译一下 它是一种分层程序表示,根据编程语言的语法表示源代码结构,每个AST节点都对应一个源代码节点
复制代码
看到这里,想必你和我一样是懵的状态,这句描述不足以在我们脑海里形成对它的具象化理解
让我们从1个小例子开始着手,如果我们有这样一段代码
function calc(a, b) { return a + b; }
复制代码
因为只是计算加法,我们想把函数名改成 sum,我们能怎么做?
这里肯定会有人想到,用 字符串的 replace 方法来替换。
好,我们创建1个 transform 方法,来实现这个能力
const code = `
function calc(a, b) {
return a + b;
}
`;
function transform (originalCode) {
if (typeof originalCode !== 'string') {
return originalCode;
}
return originalCode.replace(/calc/, 'add');
}
console.log(transform(code));
// 输出
// function add(a, b) {
// return a + b;
// }
复制代码
好像挺简单的,那我们把例子再改变一下,现在变成这个样子
function plus (args = []) {
return args.reduce((total, item) => total + item, 0);
}
function subtract (args = []) {
return args.reduce((total, item) => total - item, 0);
}
function multiply (args) {
return args.reduce((total, item) => total * item, 0);
}
function divide (args) {
return args.reduce((total, item) => total / item, 0);
}
function calc(type, ...args) {
switch (type) {
case 'plus':
return plus(args);
case 'subtract':
return subtract(args)
case 'multiply':
return multiply(args)
case 'divide':
return divide(args)
}
}
复制代码
如果我们想给 console.log 默认添加所在函数名的输出,又或者给 switch 语句添加一个 default 返回,是不是有点麻烦了
编程语言的实际场景更为复杂,运用 AST 去处理是目前主流的方式,AST 是描述代码的虚拟节点树
要打比方的话,就像是 React 的虚拟 dom,用来描述 html 中真实的 dom 节点,是一样的道理,
这种抽象化的树结构便于我们去处理它,从而得到期望的结果。
开头的那个小例子,AST 是怎么表示的它呢
// 原代码
function calc(a, b) {
return a + b;
}
复制代码
// AST表示
{
"type": "Program",
"start": 0,
"end": 38,
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 38,
"id": {
"type": "Identifier",
"start": 9,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 13
},
"identifierName": "calc"
},
"name": "calc"
},
"generator": false,
"async": false,
...
}
复制代码
至于 Program、Identifier 这些都是什么,可以先不用关心,后面会一一道来,现在只要知道 AST 描述的是完整的代码结构就可以了,读完第二部分编译器的代码实现,你就能理解它了
这里的解析器用的是 @babel/parser,解析器有很多,babel 这一整套工具是目前使用比较多的,其他工具我们后面再介绍
是一个可视化的 ast 展示网站,可以将代码粘贴到上面,观察 AST 的构成
可以在这里查看目前主流使用的各种 parser、transformer 工具库
编译器的构成与实现
编译器的构成
上面我们已经知道AST了,我们通过200多行代码实现1个编译器,深入了解一下 AST 在其中发挥的作用
大部分编译器的工作流程,大致可以分为3步:
1.解析(Parsing)=> 2.转换(Transformation)=> 3.代码生成(Code Generation)
复制代码
1、解析(Parsing)
解析就是将源码转换成AST的过程,其中又分为两步,词法分析和语法分析
词法分析
词法分析需要从头到尾,遍历一遍源码,并将源码拆分成可表达语义的最小单元 token,方便后面生成AST,
什么是语义化的最小单元和token呢,我们还拿开头的例子来说明
function calc(a, b) {
return a + b;
}
复制代码
像 calc 就是最小语义词,如果再往下拆分成 c、a、l、c,它只是普通字母了,我们并不知道它们之间有什么关联,
同理,function、return 都是不可再拆分的最小语义词,那什么是token呢
token就是 { type: ‘xxx’, value: ‘xxx’ } 这种格式的数据,可以方便生成AST
[
{ type: 'string', value: 'function' },
{ type: 'name', value: 'calc' },
{ type: 'paren', value: '(' },
{ type: 'string', value: 'a' },
{ type: 'string', value: 'b' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: '{' },
{ type: 'string', value: 'return' },
{ type: 'string', value: 'a' },
{ type: 'operator', value: '+' },
{ type: 'string', value: 'b' },
{ type: 'paren', value: '}' },
]
复制代码
语法分析
通过分析词法分析生成的 tokens,将其生成AST,Program 类型是顶层节点
{
"type": "Program",
"start": 0,
"end": 38,
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 38,
"id": {
"type": "Identifier",
"start": 9,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 13
},
"identifierName": "calc"
},
"name": "calc"
},
"generator": false,
"async": false,
...
}
复制代码
2、转换(Transformation)
下一阶段是转换阶段,这个阶段主要是接收 AST 作为参数,遍历AST,进行我们想要的更改,并生成1个新的AST
3、代码生成(Code Generation)
最后是代码生成阶段,通过遍历转换阶段的新 AST,最后得到我们想要输出的代码结构
// 源码
function calc(a, b) {
return a + b;
}
// 生成代码
function add(a, b) {
return a + b;
}
复制代码
编译器的实现
这里就是本文的重头戏了,我们从零开始实现1个编译器,了解一下每个阶段的实现细节
我们来实现一个跨语言的转换器,将 Lisp 函数调用编译成 JavaScript 方式的函数调用
Lisp | JavaScript | |
---|---|---|
2+2 | (add 2 2) | add(2, 2) |
2+(5-3) | (add 2 (subtract 5 3)) | add(2, subtract(5, 3)) |
不了解 Lisp 没关系,我们主要关注的是转换过程,怎样完成语法的转换
这个对应关系很重要,我们下来所有的操作都是基于表格中的对应关系
前面有提到编译器的实现有 3 个阶段:
1.解析(Parsing)=> 2.转换(Transformation)=> 3.代码生成(Code Generation)
复制代码
我们根据阶段定义对应的函数
1、解析
tokenizer:词法分析
parser:语法分析
2、转换
traverser:提供visitor接口,遍历AST树并修改生成新树
transformer:调用traverser
3、代码生成
codeGenerator::生成代码
1、解析
tokenizer:词法分析
/**
* 再来重温一下,我们的输入和期望输出
* 输入的源码:
*
* (add 2 (subtract 4 2))
*
* 上面代码产生的 token 会像下面这样:
*
* [
* { type: 'paren', value: '(' },
* { type: 'name', value: 'add' },
* { type: 'number', value: '2' },
* { type: 'paren', value: '(' },
* { type: 'name', value: 'subtract' },
* { type: 'number', value: '4' },
* { type: 'number', value: '2' },
* { type: 'paren', value: ')' },
* { type: 'paren', value: ')' },
* ]
*
*/
// 我们接收源码,并设置两个变量 current、tokens
function tokenizer(input) {
// 词法分析需要从头到尾遍历代码,current 变量用于表示当前遍历的位置,就像虚拟光标一样
let current = 0;
// tokens 数组用于收集 token
let tokens = [];
// 我们创建一个 while 循环来进行源码的遍历
// 因为 token 长度不同,在 1 次循环中,current 可能会增加多次
while (current < input.length) {
// 缓存 input 中的 current 字段
let char = input[current];
// 我们需要检测的第一个情况就是开括号,这在之后会被函数调用 CallExpression 所用到。但是
// 现在我们只需要关心字符即可。
//
// 我们检测是否有一个开括号
if (char === '(') {
// 如果字符为开括号,我们 push 1个新 token,type 为 paren,value 为 (
tokens.push({
type: 'paren',
value: '(',
});
// current 虚拟光标往前移动一步
current++;
// 然后继续下个循环
continue;
}
// 下面我们监测一下闭合括号,这里和前面一样,确认的话,新增 1 个 token,增加 current,继续下个循环
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 我们现在要检测空格,这里很有趣,因为空格存在的意义是分离字符,实际上对于 token 的存储并不重要,我们这里略过它就行了
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 下种 token 类型是数字,这里和前面不同,因为数字可能是连续的
//
// (add 123 456)
// ^^^ ^^^
// 只能分出两个 token
//
// 所以当我们遇到第一个数字时,进行下面的处理
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// 我们创建1个 value 变量来存储字符
let value = '';
// 我们不断向后循环,直到遇到的字符不是数字
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 推送 1 个数字类型的 token 到 tokens
tokens.push({ type: 'number', value });
// 继续下个循环
continue;
}
// 我们的语言也支持字符串,1 段被 " 包裹起来的文本
//
// (concat "foo" "bar")
// ^^^ ^^^ string tokens
//
// 我们开始做引号的检测
if (char === '"') {
// 用 value 来收集字符串 token
let value = '';
// 我们跳过双括号本身,从后面 1 位开始
char = input[++current];
// 向后迭代查找,直到找到另一个 "
while (char !== '"') {
value += char;
char = input[++current];
}
// 跳过闭合的双引号
char = input[++current];
// 我们添加 string token 到 tokens
tokens.push({ type: 'string', value });
continue;
}
// 最后 1 种 token 是 name token,也是一个字母序列,在 lisp 中是函数名
//
// (add 2 4)
// ^^^
// Name token
//
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
// 我们遍历循环字母,添加到 value 变量
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
// 我们添加 name token 并继续循环
tokens.push({ type: 'name', value });
continue;
}
// 最后我们进行语法校验,如果我们没有匹配到结果,则抛出错误,停止执行
throw new TypeError('I dont know what this character is: ' + char);
}
// tokenizer 结束时,我们返回 tokens
return tokens;
}
复制代码
parser:语法分析
/**
* 我们尝试将 tokens 数组解析成 AST(抽象语法树)
*
* [{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
*/
// 我们定义 parser 函数来接收 tokenizer 产出的 tokens
function parser(tokens) {
// 我们还是创建 current 变量来表示当前虚拟光标的位置
let current = 0;
// 我们创建 AST 的根节点,Program 是 AST 的顶层节点
let ast = {
type: 'Program',
body: [],
};
// 我们会遇到函数嵌套这种情况,所以这次我们用递归代替 while 循环
function walk() {
// 提取虚拟光标位置的token
let token = tokens[current];
// 我们检测是否为 number 类型的 token
if (token.type === 'number') {
// 如果是 number,current +1
current++;
// 我们返回 1 个 AST node 叫 NumberLiteral(数字字面量)
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 如果是 string token,我们像 number 一样,创建 1 个 StringLiteral(字符串字面量) node
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
// 接下来我们看 CallExpressions(调用表达式),它是从 1 个开括号开始的
if (
token.type === 'paren' &&
token.value === '('
) {
// current +1,跳过开括号,我们不关心它
token = tokens[++current];
// 我们创建一个 type 为 CallExpression 的基础 node 节点,设置 name 为 token 的 value
// 开括号后面就是函数名
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
// current +1,我们跳过函数名,直接看表达式
token = tokens[++current];
// 为了收集 CallExpression 的 params,我们往后查询每个 token,直到遇到闭合括号
//
// 这就是需要递归的时候。我们使用递归而不是试图直接分析可能有无限多层嵌套节点的参数。
//
// 为了解释这个概念,以我们的 lisp 代码为例。你可以观察到 add 的参数是一个数字和一个嵌套
// 的 CallExpression,而这个 CallExpression 又拥有自己的参数。
//
// (add 2 (subtract 4 2))
//
// 你应该注意到了,我们的 tokens 数组有多个闭合括号
//
// [
// { type: 'paren', value: '(' },
// { type: 'name', value: 'add' },
// { type: 'number', value: '2' },
// { type: 'paren', value: '(' },
// { type: 'name', value: 'subtract' },
// { type: 'number', value: '4' },
// { type: 'number', value: '2' },
// { type: 'paren', value: ')' }, <<< 闭合括号
// { type: 'paren', value: ')' }, <<< 闭合括号
// ]
//
// 我们依赖 walk 函数来增加 current
// 所以我们创建 1 个 while 循环,继续循环token,直到遇到 1 个闭合括号
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// 我们调用 walk 函数直到返回 1 个 node 节点,并且我们将 push 它到 node.params
node.params.push(walk());
token = tokens[current];
}
// 最后,我们将 current+1,跳过闭合括号
current++;
// 返回 node
return node;
}
// 还是如此,如果 token 没有匹配,抛出类型错误
throw new TypeError(token.type);
}
// 然后我们启动 walk 函数,推送 nodes 到 ast.body
//
// 我们在1个循环里做的原因,是因为 CallExpression(函数调用表达式) 可能是多个,且互不相关
//
// (add 2 2)
// (subtract 4 2)
//
while (current < tokens.length) {
ast.body.push(walk());
}
// parser 的最后,我们返回 AST
return ast;
}
复制代码
2、转换
traverser:提供visitor接口,遍历AST树并修改生成新树
/**
* 现在我们有AST了,并且我们可以用访问者模式来访问不同的 node 节点,
* 当我们遇到匹配的 type 时,调用 visitor 上不同的方法,
* 访问者模式是一种数据操作与数据操作分离的设计模式,
* 如下方,我们需要在 enter、exit中处理节点
*
* traverse(ast, {
* Program: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
*
* CallExpression: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
*
* NumberLiteral: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
* });
*/
// 我们定义 traverser 函数来接收 AST 和 1个visitor,里面我们将要定义两个函数
function traverser(ast, visitor) {
// traverseArray 函数将允许我们遍历数组,我们下面还要定义 1 个 traverseNode 函数
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// traverseNode 将接收 node 节点和它们的父 node 节点
// 可以把它们两个作为参数传递给我们的 visitor 方法,也就是
function traverseNode(node, parent) {
// 初始时,看看node type是否有对应的 visitor 方法
let methods = visitor[node.type];
// 如果有 enter 方法,我们就调用它,并传递两个参数
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 接下来,我们根据当前 node type 的不同,需要区别处理
switch (node.type) {
// 开始时,最顶层的结构是 Program,它有名为 body 的属性,
// 我们使用 traverseArray 递归处理所有子节点
case 'Program':
traverseArray(node.body, node);
break;
// 接下来,我们对 CallExpression 一样处理,循环它的 params,也就是下面的表达式体
case 'CallExpression':
traverseArray(node.params, node);
break;
// NumberLiteral 和 StringLiteral,它们下面已经没有子节点了,就不用额外处理了
case 'NumberLiteral':
case 'StringLiteral':
break;
// 如果没有触发上面的匹配,和之前一样抛出 1 个类型错误
default:
throw new TypeError(node.type);
}
// 如果 visitor 上有 exit 方法,我们执行它
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 最后我们用 AST 作为参数启动 traverseNode 函数,初始时在最顶层,所以是没父节点的
traverseNode(ast, null);
}
复制代码
transformer:调用traverser
/**
* 接下来是 transformer,我们遍历 AST 节点树,通过 visitor 上的方法进行处理,生成新的 AST 树
*
* ----------------------------------------------------------------------------
* 源AST | 转换后的AST
* ----------------------------------------------------------------------------
* { | {
* type: 'Program', | type: 'Program',
* body: [{ | body: [{
* type: 'CallExpression', | type: 'ExpressionStatement',
* name: 'add', | expression: {
* params: [{ | type: 'CallExpression',
* type: 'NumberLiteral', | callee: {
* value: '2' | type: 'Identifier',
* }, { | name: 'add'
* type: 'CallExpression', | },
* name: 'subtract', | arguments: [{
* params: [{ | type: 'NumberLiteral',
* type: 'NumberLiteral', | value: '2'
* value: '4' | }, {
* }, { | type: 'CallExpression',
* type: 'NumberLiteral', | callee: {
* value: '2' | type: 'Identifier',
* }] | name: 'subtract'
* }] | },
* }] | arguments: [{
* } | type: 'NumberLiteral',
* | value: '4'
* ---------------------------------- | }, {
* | type: 'NumberLiteral',
* | value: '2'
* | }]
* | }
* | }
* | }]
* | }
* ----------------------------------------------------------------------------
*/
// 定义 transformer 函数接收 Lisp AST
function transformer(ast) {
// 我们创建 newAst,像前面的 AST 一样,都有 Program 类型的根节点
let newAst = {
type: 'Program',
body: [],
};
// 接下来我们做 hack 写法,我们在旧 AST 上挂 1 个 _content 方法,用来存储新 AST 的引用,
// 只要记住它是一个引用就行了,通常可以有更好的抽象方法,这里我们尽量用简单的方法处理
ast._context = newAst.body;
// 我们调用 traverser 函数,用 ast、visitor 函数作为参数
traverser(ast, {
// 第一个 visitor 方法用来处理 NumberLiteral
NumberLiteral: {
// 我们将在访问到这个节点类型时被调用
enter(node, parent) {
// 新节点也是叫 NumberLiteral,我们将它 push 到新的 AST 树上
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 接下来处理 StringLiteral
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// 现在轮到 CallExpression
CallExpression: {
enter(node, parent) {
// 我们创建新语言的 CallExpression 表达式结构
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 现在我们定义新的 _context 在原始 CallExpression node 上,将想转换的 node 节点 expression.arguments 挂载上去
node._context = expression.arguments;
// 那么,我们确认一下父节点是否是 CallExpression
if (parent.type !== 'CallExpression') {
// 我们用 ExpressionStatement node 包裹 CallExpression node,
// 为什么这么做呢,因为在 JavaScript 语言中,顶层节点应该是个表达式声明
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// 最后我们推送 CallExpression 到父节点的 _context 中,它的父节点可能是另一个函数调用
parent._context.push(expression);
},
}
});
// 最后我们返回新创建的AST
return newAst;
}
复制代码
3、代码生成
codeGenerator::生成代码
/**
* 现在让我们看最后一步:代码生成
* 递归调用它自身的所有子节点,最终通过 AST 树生成目标字符串
*/
function codeGenerator(node) {
switch (node.type) {
// 如果是 Program 根节点. 我们遍历其所有的子节点
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// ExpressionStatement 是包裹层,我们在最后面加 1 个 ;,继续处理它的表达式体
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';' // << (...因为我们想让它是正确的语法格式)
);
// 对于 CallExpression,我们将打印它的 callee,拼接 (,map遍历处理它的 arguments,最后再加上 )
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// 对于 Identifier,我们返回 node 的 name 即可
case 'Identifier':
return node.name;
// 对于 NumberLiteral,我们返回 node 的 value 即可
case 'NumberLiteral':
return node.value;
// 对于 NumberLiteral,我们用 "" 包裹 node 的 value 再返回
case 'StringLiteral':
return '"' + node.value + '"';
// 如果未匹配到处理规则,抛出类型错误
default:
throw new TypeError(node.type);
}
}
复制代码
4、将1,2,3阶段方法,封装成 compiler
/**
* 最终,我们创建 1 个 compiler(编译器) 函数
* 这里,我们用这样的路径将它们串起来,这样更加直观
*
* 1. input => tokenizer => tokens
* 2. tokens => parser => ast
* 3. ast => transformer => newAst
* 4. newAst => generator => output
*/
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
return output;
}
复制代码
再回忆一下我们前面这张表,是不是脑海中有思维模型了
Lisp | JavaScript | |
---|---|---|
2+2 | (add 2 2) | add(2, 2) |
2+(5-3) | (add 2 (subtract 5 3)) | add(2, subtract(5, 3)) |
编译器完整示例代码:点这里获取
Babel是怎样通过AST转义的?
再回想一下编译器的 3 个阶段:
1.解析(Parsing)=> 2.转换(Transformation)=> 3.代码生成(Code Generation)
复制代码
babel在每个阶段都有对应的工具包
1.解析(Parsing)=> [@babel/parser](https://babeljs.io/docs/en/babel-parser)
2.转换(Transformation)=> @babel/traverse(https://babeljs.io/docs/en/babel-traverse)
3.代码生成(Code Generation)=> @babel/generator(https://babeljs.io/docs/en/babel-generator)
复制代码
AST的结构相对复杂繁琐,为了便于操作 AST 的节点,babel 还有 1 个 @babel/types 的包
我们继续使用第二个例子,将 console 加上函数名的输出,方便辨别打印的信息
function plus (args = []) {
return args.reduce((total, item) => total + item, 0);
}
function subtract (args = []) {
return args.reduce((total, item) => total - item, 0);
}
function multiply (args) {
return args.reduce((total, item) => total * item, 0);
}
function divide (args) {
console.log(args);
return args.reduce((total, item) => total / item, 0);
}
function calc(type, ...args) {
console.log(type);
console.log(args);
switch (type) {
case 'plus':
return plus(args);
case 'subtract':
return subtract(args)
case 'multiply':
return multiply(args)
case 'divide':
return divide(args)
}
}
复制代码
附上完整代码
const t = require('@babel/types');
const babelParser = require('@babel/parser');
const generate = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;
const code = `
function plus (args = []) {
return args.reduce((total, item) => total + item, 0);
}
function subtract (args = []) {
return args.reduce((total, item) => total - item, 0);
}
function multiply (args) {
return args.reduce((total, item) => total * item, 0);
}
function divide (args) {
console.log(args);
return args.reduce((total, item) => total / item, 0);
}
function calc(type, ...args) {
console.log(type);
console.log(args);
switch (type) {
case 'plus':
return plus(args);
case 'subtract':
return subtract(args)
case 'multiply':
return multiply(args)
case 'divide':
return divide(args)
}
}
`;
// 1.解析
const ast = babelParser.parse(code);
// 2.转换
// 节点类型及数据结构,可以查看 estree 规范:https://github.com/estree/estree
traverse(ast, {
// 进入节点时,会调用该visitor
enter(path) {
console.log('Log: path.node.type ->', path.node.type);
},
// 退出节点时, 会调用该visitor
exit(path) {
// console.log('exit')
},
// 可以直接写节点类型,如果扫描到该类型,会调用该visitor
CallExpression(path) {
const { callee } = path.node;
// 判断是否为 console.log 且有父节点
const isConsoleLog = t.isMemberExpression(callee) && callee.object.name === 'console' && callee.property.name === 'log';
if (isConsoleLog) {
// 如果父节点是声明函数,取它的函数名,添加到 console 中
const funcPath = path.findParent(p => p.isFunctionDeclaration());
if (funcPath) {
const funcName = funcPath.node.id.name;
path.node.arguments.unshift(t.stringLiteral(funcName));
}
}
}
})
// 3.代码生成
const output = generate(ast)
console.log('Input \n', code)
console.log('----------------------')
console.log('Output \n', output.code)
复制代码
执行一下,发现在 calc 函数内的 console.log,已经添加了函数名,符合我们的预期
function calc(type, ...args) {
console.log("calc", type);
console.log("calc", args);
...
}
复制代码
AST的介绍就到此为止了,你是否有一些收获了呢
最后,强烈建议花点时间思考一下第二部分的代码
参考资料: