目录
- AST 的基本结构
- 安装 babel 库
- babel 中的组件
- parse 与 generator
- parse
- generator
- 完整代码
- traverse 与 visitor
- traverse
- visitor 的定义方式
- path 对象中的 traverse
- types
- 判断节点类型
- 生成新的节点
- valueToNode(方便的生成字面量)
- path 对象(重点)
- path 与 Node 的区别
- path中的方法
- skip 与 stop 与 return
- 获取子节点 / path.get()
- 判断 path 类型 / path.is....
- 节点转代码 / path.toString() / path + ''
- 替换节点属性
- 替换整个节点 replaceWith() / replaceWithMutiple() / replaceInline() / replaceWithSourceString()
- replaceWith()
- replaceWithMutiple()
- replaceInline()
- replaceWithSourceString()
- 删除节点 remove
- 插入节点 insertBefore / insertAfter
- 父级path: parent / parentPath
- 同级 path
- 容器 container
- path.inList
- path.container / path.listKey / path.key
- path.getSibling(index)
- path.getAllNextSiblings
- path.getNextSibling
- path.getAllPrevSiblings
- path.getPrevSibling
- unshiftContainer 与 pushContainer
- scope 详解
- 获取标识符作用域范围 scope.block
- 获取函数作用域范围 scope.block 方式获取
- scope.getBinding()
- scope.getOwnBinding()
- referencePaths
- constantViolations
- scope.rename(标识符重命名)
- scope.generateUidIdentifier()
- scope 的其他方法
- 遍历作用域
AST 的基本结构
JS 代码解析成 AST 以后,类似于中间形式的 JSON 数据
经过 Babel 解析以后,通常将里面的元素叫做节点(Node)
- Babel 提供了很多方法去操作这些节点
demo.js
let num1 = 1;
let num2 = 2;
AST explorer
body 中的元素,被称之为节点(Node)
每个节点都有自己对应的属性名,属性值
安装 babel 库
npn install @babel/core
// file_name: demo.js
let num1 = 1;
let num2 = 2;
// file_name: AST.js
// 安装 babel 库: npm install @babel/core
const fs = require('fs')
const traverse = require('@babel/traverse').default // 用于遍历 AST 节点
const types = require('@babel/types') // 用于判断, 生成 AST 节点
const parser = require('@babel/parser') // 将js代码解析成ast节点
const generator = require('@babel/generator').default // 将ast节点转换成js代码
// 读取
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code) // 将js代码解析成ast语法树
// 在这里对 ast 进行一系列操作
traverse(ast,{
VariableDeclaration(path){
console.log(path + '') // 打印当前遍历到的节点
}
})
let js_code = generator(ast).code // 将ast节点转换成js代码
// 写入
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
babel 中的组件
parse 与 generator
parse
parse 用来将 JS 代码转换成 AST 节点
// 节点的信息一般都是通过网站查看的 https://astexplorer.net/
// 网站上使用的 parser 需要和代码中的 parser 匹配,结构才能一样
let ast = parser.parse(ast_code, {
// 当解析的代码中有 import 字眼的时候,需要指定 sourceType 为true
// 这种情况很少见(很少使用)
sourceType: true,
}) // 将 js代码 转换成 ast
generator
generator 用来将 AST节点 转换成 JS代码
// generator 实际上是一个对象,对象中code属性就是转换好的js代码(最常使用的方式)
let js_code = generator(ast, {
retainLines: false, // 是否使用与源代码相同的行号,默认false
comments: true, // 是否保留注释,默认true
compact: false, // 是否压缩,默认 false
}).code // 将 ast 转换成 js代码
完整代码
// file_name: demo.js
let num1 = 1;
let num2 = 2;
// AST.js
const fs = require('fs')
const parser = require('@babel/parser') // 将js代码解析成ast节点
const generator = require('@babel/generator').default // 将ast节点转换成js代码
// 读取
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code) // 将js代码解析成ast语法树
let js_code = generator(ast).code // 将ast节点转换成js代码
console.log(js_code)
traverse 与 visitor
traverse
traverse 用来遍历 ast 节点,简单地说就是把 ast 上的各个节点都走一遍.
单纯的把节点都走一遍是没有意义的,所以 traverse 需要配合 visitor 使用
// file_name: demo.js
let Obj = {
name: 'xiaojiangbang',
add: function(){
return a + b + 1000;
},
mul: function (a, b) {
return a * b + 1000;
}
}
将代码中的 标识符a 修改成 x
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
// 读取
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code)
let visitor = {
// 遍历语法树中所有的 Identifier 节点
Identifier: function(path){
// 传进来的参数是path对象
// 通过path.node可以得到当前的节点
// path.node.name 可以得到标识符对应的名字
console.log(path.node.name);
// path.parentPath.toString() 可以得到当前节点的父节点对应的js代码
console.log(path.parentPath.toString())
console.log('==============================')
if (path.node.name === 'a'){ // 判断
path.node.name = 'x'; // 修改
}
}
}
// 将代码中的标识符 a 修改成 x
traverse(ast,visitor)
let js_code = generator(ast).code
// 写入
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
运行后的代码
visitor 的定义方式
定义方式(常用的定义方式 visitor2 / visitor3
// 最常用的是 visitor1 / visitor2
// ES6 之前的语法
const visitor1 = {
Identifier: function(path){
console.log(path.node.name)
}
}
// ES6 的语法
const visitor2 = {
Identifier(path){
console.log(path.node.name)
}
}
// ES6 的语法
// 深度优先的遍历方式
// visitor3 中存在一个重要的 enter,在遍历节点过程中,实际上有两次机会来访问一个节点
// 进入节点时(enter) 退出节点时(exit)
// 可以只选择一个,指明是进入节点还是退出节点时
const visitor3 = {
Identifier:{
enter(path){
// 进入节点时
console.log('enter: ', path.node.name)
},
exit(path){
// 退出退出节点时
console.log('exit: ', path.node.name)
}
}
}
// 如果存在父子节点,
// enter 的处理时机是先处理父节点,再处理子节点
// exit 的处理时机是先处理子节点,再处理父节点
// traverse 默认就是在 enter 时候处理,如果要在 exit 时候处理,必须在 visitor 中指明
把同一个函数应用到多个节点(不常用)
const visitor = {
// 同时遍历3个节点,不管碰到哪个都会进入函数执行 console.log(path.node.type)
// 如果同时有更多个节点需要处理的,则继续在 | 后面加上类型
"FunctionExpression|BinaryExpression|Identifier": function(path){
console.log(path.node.type)
}
}
将多个函数应用于同一个节点(不常用)
function fuc1(path){
console.log('fuc1: ', path.node.name)
}
function fuc2(path){
console.log('fuc2: ', path.node.type)
}
const visitor = {
Identifier: {
enter: [fuc1, fuc2] // 相当于一个节点执行2个函数(不限函数数量),从左到右依次执行
}
}
path 对象中的 traverse
traverse 是将传入的 ast 节点从头跑一遍(全局遍历)(最常用的方式)
path 对象中的 traverse 是从 traverse 遍历到的节点中再继续向下遍历
file_name: demo.js
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function(a, b){
return a + b + 1000;
},
mul: function (a, b){
return a * b + 1000;
}
}
将函数内部的标识符 a 改成 x
按照我们之前的方式,遍历 Identifier 节点,将标识符 a 改成 x,全局变量 a 的标识符也会改成 x
const fs = require('fs')
const traverse = require('@babel/traverse').default
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code)
//
let visitor = {
// 先找到所有的函数, 然后将函数内部的标识符 a 改为 x
FunctionExpression: function(path){
// path.traverse 可以将遍历的范围限制在局部
// 使用 path 对象的 traverse 方法
// 从当前遍历到的节点再往下选择节点去遍历并处理
let func_visitor = {
Identifier(path) {
if (path.node.name === 'a') {
path.node.name = 'x'
}
}
}
path.traverse(func_visitor)
}
}
traverse(ast, visitor)
//
let js_code = generator(ast, {
retainLines: false,
comments: true,
compact: false,
}).code
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
将函数中的第一个参数改成标识符 z
let visitor = {
FunctionExpression: function(path){
// 函数中的参数都是放在 type 为 FunctionExpression 中的 params 节点中类型为 list
// 第一个参数就为 params[0], 取到这个参数对应的标识符名字就为 params[0].name
// console.log(path.node.params[0].name)
let par_name = path.node.params[0].name
// 再使用 path.traverse 进行局部遍历
path.traverse(
{
Identifier(p){
// 如果传进来的参数和外部取到函数第一个参数标识符的名字一致
// 则将改标识符名字改为 x
console.log(this.par_name) // this.par_name 为 path.traverse 第二个参数
if(p.node.name === this.par_name) { p.node.name = 'z' };
}
},
{par_name:par_name}
)
}
}
types
判断节点类型
判断节点类型有三种方式
- node节点的type属性
- types组件的方法(传入node节点类型)
- path对象的方法(判断path对象下的类型,一般情况下和node节点的类型是一致的)
types组件判断节点类型的方法
- is类型
- 返回布尔值
- asert类型
- 当前节点不符合要求,会抛出异常
最常用的是 is的版本,返回布尔值
// file_name: demo.js
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function(a, b){
return a + b + 1000;
},
mul: function (c, b){
return c * b + 1000;
}
}
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code)
//
traverse(ast, {
// 遍历所有ast节点,遍历到节点的时候都会进入函数
enter(path) {
// 方式一, path.node.type 判断类型
// if(path.node.type === 'Identifier' && path.node.name === 'a'){
// console.log(path.node.name)
// path.node.name = 'x'
// }
// 方式二, type.is.... 判断节点类型
// (
// 参数1: node节点,
// 参数2: 可以用来传入一个对象,可以判断node节点指定属性的值
// )
// if (types.isIdentifier(path.node, {name: 'a'})) {
// console.log(path.node.name)
// path.node.name = 'x'
// }
// 方式三 path.is.... 判断节点类型
// (参数1: 可以用来传入一个对象,可以判断node节点指定属性的值)
if (path.isIdentifier({name: 'a'})) {
console.log(path.node.name)
path.node.name = 'x'
}
}
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
生成新的节点
// file_name: AST.js
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
//
/*
生成代码示例
let a = 100;
let obj = {
a: 200,
add: function(a, b){
return a + b + 1000;
},
mul: function (c, b){
return c * b + 1000;
}
}
*/
// 编辑器建议使用 vscode,如果有报错删了重装 babel 库
// ctrl + 鼠标左键 方法名 可以进入对应的方法,查看生成该节点需要什么参数类型
// 生成标识符 a, obj, add, mul, b, c
// declare function identifier(name: string): Identifier;
// 生成 identifier 节点需要传入 字符串
ide_a = types.identifier('a') // 标识符 a
ide_obj = types.identifier('obj') // 标识符 obj
ide_add = types.identifier('add') // 标识符 add
ide_mul = types.identifier('mul') // 标识符 mul
ide_b = types.identifier('b') // 标识符 b
ide_c = types.identifier('c') // 标识符 c
// https://astexplorer.net/
// 将代码放到网站上查看语法树生成 let a = 100 需要什么节点类型;
// declare function variableDeclaration(kind: "var" | "let" | "const" | "using" | "await using", declarations: Array<VariableDeclarator>): VariableDeclaration;
// declare function variableDeclarator(id: LVal, init?: Expression | null): VariableDeclarator;
let demo_1 = types.variableDeclaration('let', [types.variableDeclarator(ide_a, types.numericLiteral(100))]) // let a = 100;
console.log(generator(demo_1).code) // 生成的节点为 node 节点,所以需要 generator 组件将 ast 语法树转换成js代码
// let a = 100;
// declare function objectExpression(properties: Array<ObjectMethod | ObjectProperty | SpreadElement>): ObjectExpression;
// declare function objectProperty(key: Expression | Identifier | StringLiteral | NumericLiteral | BigIntLiteral | DecimalLiteral | PrivateName, value: Expression | PatternLike, computed?: boolean, shorthand?: boolean, decorators?: Array<Decorator> | null): ObjectProperty;
// declare function functionExpression(id: Identifier | null | undefined, params: Array<Identifier | Pattern | RestElement>, body: BlockStatement, generator?: boolean, async?: boolean): FunctionExpression;
// declare function blockStatement(body: Array<Statement>, directives?: Array<Directive>): BlockStatement;
// declare function returnStatement(argument?: Expression | null): ReturnStatement;
// declare function binaryExpression(operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=" | "|>", left: Expression | PrivateName, right: Expression): BinaryExpression;
let obj_1 = types.objectProperty(ide_a, types.numericLiteral(200)) // a: 200,
let obj_2 = types.objectProperty(ide_add,
types.functionExpression(
null,
[ide_a, ide_b],
types.blockStatement([
types.returnStatement(types.binaryExpression(
"+",
ide_a,
types.binaryExpression("+", ide_b, types.numericLiteral(1000),
)
))
])
)) // add: function (a, b) {return a + (b + 1000);}
let obj_3 = types.objectProperty(ide_mul,
types.functionExpression(
null,
[ide_c, ide_b],
types.blockStatement([
types.returnStatement(types.binaryExpression(
"*",
ide_c,
types.binaryExpression("+", ide_b, types.numericLiteral(1000),
)
))
])
)) // mul: function (c, b){return c * b + 1000;}
let demo_2 = types.variableDeclaration('let', [types.variableDeclarator(ide_obj, types.objectExpression([obj_1, obj_2, obj_3]))])
// 将生成的节点转换成 js代码并打印
console.log(generator(demo_2).code)
/*
let obj = {
a: 200,
add: function (a, b) {
return a + (b + 1000);
},
mul: function (c, b) {
return c * (b + 1000);
}
};
*/
valueToNode(方便的生成字面量)
支持生成的类型
- undefined undefined
- boolean 布尔值
- null 空
- string 字符串
- number 数值
- RegExp 正则表达式
- ArrayExpression 数组表达式
- object 对象
- Expression 表达式语句
字面量可以用 valueToNode 可以很方便的生成
const types = require('@babel/types')
const generator = require('@babel/generator').default
console.log(types.valueToNode(100)); // 生成numericliteral节点 数值字面量100
console.log(types.valueToNode('xiaojianbang')) // 生成stringliteral节点 字符串xiaojianbang
array_node = types.valueToNode([ // 数组表达式
undefined, // undefined
true, // 布尔值
null, // null
'xiaojianbang', // 字符串
100, // 数值
/\w\s/g, // 正则表达式
{a:100} // 对象
])
console.log(generator(array_node).code)
path 对象(重点)
path 与 Node 的区别
// file_name: demo.js
let Obj = {
name: 'xiaojiangbang',
add: function(a, b){
return a + b + 1000;
},
mul: function (a, b) {
return a * b + 1000;
}
}
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code)
//
traverse(ast, {
Identifier(path){
console.log(path.node) // node 是path对象里的一个属性
console.log(path.parentPath) // parentPath 是 path 对象里的父节点属性
console.log(path.parent) // parent 是 parentPath 对象里的一个属性
// parentPath 与 parent 和 path 与 node 的关系是一样的,都是 path 对象里的属性
path.stop() // path对象下的方法,当触发这个方法的时候就会停止遍历
}
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
path中的方法
skip 与 stop 与 return
path.stop()
- 将本次遍历执行完毕后,停止后续节点遍历(会将本次遍历代码完整执行)
path.skip()
- 执行节点替换操作后,traverse 依旧能够遍历
- path.skip() 可以跳过替换后的节点遍历,避免不合理的递归调用,不影响后续节点遍历(skip位置后面代码会执行)
return
- 跳过当前遍历的后续代码,不影响后续节点遍历(相当于continue)
- 不建议使用,推荐使用 path
获取子节点 / path.get()
//
traverse(ast, {
BinaryExpression(path){
// 获取子节点
console.log(path.node.left)
console.log(path.node.right)
console.log(path.node.operator)
// 获取子path
console.log(path.get('left'))
console.log(path.get('right'))
console.log(path.get('operator')) // 实际上只是一个 + 号,但是也被包装成了path对象进行返回
console.log(path.get('left.left.name')); // 如果存在多级访问,需要以 . 连接
// 这样子的操作是没有意义的
// 假如你获取到的新的节点,后续还要调用 path 的方法去进行访问的话,那么就可以通过get去获取
path.stop()
}
}
)
//
判断 path 类型 / path.is…
path对象提供相应的方法来判断自身类型,使用方法与 types 组件差不多
- types组件判断的是 node
- path对象判断的是 path
//
traverse(ast, {
BinaryExpression(path){
// 获取到的新的节点,后续还要调用path的方法去进行访问的话,那么就可以通过get去获取
console.log(path.get('left').isIdentifier()) // 是否为标识符类型
console.log(path.get('right').isNumericLiteral({value:1000})) // 是否为数值类型并且值是否为1000
path.stop()
}
}
)
//
节点转代码 / path.toString() / path + ‘’
很多时候需要在执行过程中把部分节点转为代码,而不是在最后才把整个AST转成代码
grenator组件也可以把 AST 中的一部分节点转成代码,这对节点遍历过程中的调试很有帮助
- grenator转换的是node节点
//
traverse(ast, {
FunctionExpression(path){
// generator 组件转换
console.log(generator(path.node).code) // grenator转换的是node节点
// path 对象,更简便的转换方式
console.log(path.toString());
console.log(path + '') // 隐式转换
// path 对象是复写了toString() 方法的
// path.node 只是一个普通的对象,并没有复写 toString 方法
console.log(path.node + '')
path.stop()
}
})
//
替换节点属性
替换节点属性与获取节点属性方法相同,只是改为赋值,但并非随意替换
- 需要注意替换的类型要在运行的类型范围内,因此需要熟悉 ast 的结构
//
traverse(ast, {
BinaryExpression(path){
// 注意替换的类型要在允许的类型范围内,因此需要熟悉 ast 的结构
// path.node.left = 'x' 这样替换是错误的
path.node.left = types.identifier('x')
}
})
//
替换整个节点 replaceWith() / replaceWithMutiple() / replaceInline() / replaceWithSourceString()
replaceWith()
节点替换节点,并且是一换一
//
traverse(ast, {
BinaryExpression(path){
path.replaceWith(types.valueToNode('xiaojianbang'))
path.stop()
}
})
//
运行结果
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function (a, b) {
return "xiaojianbang";
},
mul: function (c, b) {
return c * b + 1000;
}
};
replaceWithMutiple()
节点替换节点,并且是一换多
//
traverse(ast, {
BinaryExpression(path){
// 节点替换节点,并且是一换多
path.replaceWithMultiple([
types.valueToNode('xiaojianbang1'),
types.valueToNode('xiaojianbang2'),
types.valueToNode('xiaojianbang3'),
])
path.stop()
}
})
/
运行结果
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function (a, b) {
return "xiaojianbang1", "xiaojianbang2", "xiaojianbang3";
},
mul: function (c, b) {
return c * b + 1000;
}
};
replaceInline()
replaceWith() 与 replaceWithMutiple() 的结合
- 当传给的参数是一个参数的时候,就相当于是 replaceWith()
- 当传给的参数是数组的时候,就相当于是 replaceWithMutiple()
用法一
//
traverse(ast, {
BinaryExpression(path){
// 当传给的参数是一个参数的时候
// 就相当于是 replaceWith
path.replaceInline(types.valueToNode('xiaojianbang1'))
path.stop()
}
})
//
运行结果
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function (a, b) {
return "xiaojianbang1";
},
mul: function (c, b) {
return c * b + 1000;
}
};
用法二
//
traverse(ast, {
BinaryExpression(path){
// 当传给的参数是一个数组的时候(一个或多个参数)
// 相当于是 replaceWithMutiple
path.replaceInline([
types.valueToNode('xiaojianbang1'),
types.valueToNode('xiaojianbang2'),
types.valueToNode('xiaojianbang3'),
])
path.stop()
}
})
//
运行结果
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function (a, b) {
return "xiaojianbang1";
},
mul: function (c, b) {
return c * b + 1000;
}
};
replaceWithSourceString()
用字符串当成代码去替换节点
//
traverse(ast, {
ReturnStatement(path){
// 代码生成示例
/*
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function(a, b){
return function(){
return a + b + 1000
}()
},
mul: function (c, b){
return function(){
c * b + 1000
}()
}
}
*/
// 获取path对象可以很方便的把节点转为代码(隐式转换)
// path.get 方法返回的就是 path 对象
console.log(path.get('argument') + '') // path 取 return 语句的返回值
// 使用 node 节点转代码需要用到 generator 组件
// 例: ret_arg = generator(path.node.argument).code
console.log(generator(path.node.argument).code) // node 对象取 return 语句的返回值
console.log('==========')
let ret_arg = path.get('argument') + ''
path.replaceWithSourceString('function(){return ' + ret_arg + '}()')
path.skip() // 跳过替换后的节点遍历
// 因为遍历的是 ReturnStatement 节点,替换后的节点也是 ReturnStatement 节点
// path.skip 会跳过替换后的节点遍历,但是不影响后续的遍历
}
})
//
删除节点 remove
删除当前节点
//
traverse(ast, {
ReturnStatement(path){
path.remove()
}
})
//
结果
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function (a, b) {},
mul: function (c, b) {}
};
插入节点 insertBefore / insertAfter
//
traverse(ast, {
ReturnStatement(path){
path.insertBefore(types.valueToNode('insertBefore')) // 节点前插入代码
path.insertAfter(types.valueToNode('insertAfter')) // 节点后插入代码
// 可以插入多个成员,传入数组
path.insertBefore([types.valueToNode('insertBefore1'), types.valueToNode('insertBefore2'),])
path.insertAfter([types.valueToNode('insertAfter1'), types.valueToNode('insertAfter2'),])
}
})
//
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function (a, b) {
"insertBefore"
"insertBefore1"
"insertBefore2"
return a + b + 1000;
"insertAfter1"
"insertAfter2"
"insertAfter"
},
mul: function (c, b) {
"insertBefore"
"insertBefore1"
"insertBefore2"
return c * b + 1000;
"insertAfter1"
"insertAfter2"
"insertAfter"
}
};
父级path: parent / parentPath
path.parentPath.node 等价于 path.parent
//
traverse(ast, {
ReturnStatement(path){
console.log(path.parentPath.node);
console.log(path.parent)
console.log('==============================')
}
})
//
同级 path
容器 container
container(容器)
listKey(容器名)
key (当前节点在容器中的索引)
// file_name: demo.js
let a = 100;
let obj = {
name: 'xiaojianbang',
add: function(x, y){
let a = 100
let b = 200;
return x + y + a + b + 1000;
},
mul: function (c, b){
return c * b + 1000;
}
}
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code)
//
traverse(ast, {
ReturnStatement(path){
console.log(path + '');
console.log(path.container);
console.log(path.listKey);
console.log(path.key);
console.log('==========');
}
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
container 并非一直都是数组
// file_name: AST.js
const fs = require('fs')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const ast_code = fs.readFileSync('demo.js', {
encoding: 'utf-8'
})
let ast = parser.parse(ast_code)
//
traverse(ast, {
ObjectExpression(path){
console.log(path + '');
console.log('container: ', path.container);
console.log('listKey:', path.listKey);
console.log('key:', path.key);
console.log('==========')
}
})
//
let js_code = generator(ast).code
fs.writeFileSync('New_demo.js', js_code, {
encoding: 'utf-8'
})
对于这种情况,可以把它(container)当成不是容器(它是没有兄弟节点的)
分析的时候只需要关注 container 是数组的情况(代表它有兄弟(同级)节点)
path.inList
用于判断是否有同级节点
当 container 为数组,但是只有一个成员时,也会返回 true
path.container / path.listKey / path.key
path.key 获取当前节点在容器中的索引
path.container 获取容器(包含所有同级节点的数组)
path.listKey 获取容器名
path.getSibling(index)
用于获取同级path,其中参数 index 即是容器数组中的索引
path.getAllNextSiblings
获取所有后续的节点
path.getNextSibling
获取下一个节点
path.getAllPrevSiblings
获取所有前面的节点
path.getPrevSibling
获取上一个节点
unshiftContainer 与 pushContainer
unshiftContainer 在容器的头部插入节点
pushContainer 在容器的尾部插入节点
//
traverse(ast, {
ReturnStatement(path){
console.log(path.listKey)
console.log(path + '')
// 在容器的头部插入节点
path.parentPath.unshiftContainer('body', types.expressionStatement(types.stringLiteral('unshiftContainer'))) // 插入一个
path.parentPath.unshiftContainer('body', [ // 插入多个
types.expressionStatement(types.stringLiteral('unshiftContainer1')),
types.expressionStatement(types.stringLiteral('unshiftContainer2')),
types.expressionStatement(types.stringLiteral('unshiftContainer3')),
])
// 在容器的尾部插入节点
path.parentPath.pushContainer('body', types.expressionStatement(types.stringLiteral('pushContainer'))) // 插入一个
path.parentPath.pushContainer('body', [ // 插入多个
types.expressionStatement(types.stringLiteral('pushContainer1')),
types.expressionStatement(types.stringLiteral('pushContainer2')),
types.expressionStatement(types.stringLiteral('pushContainer3')),
])
path.stop()
}
})
//
//
traverse(ast, {
ReturnStatement(path){
console.log('=================path下的属性===========================');
console.log('inList: ', path.inList) // container 是否为数组
console.log('listKey', path.listKey) // 输出容器名
console.log('container: ', path.container); // 输出 container(容器)
console.log('key: ', path.key); // 输出 当前节点在容器中的索引
console.log('==================path.getSibling======================');
// path.getSibling 用于获取同级path
console.log('getSibling: ', path.getSibling(path.key)) // 输出当前节点
console.log('path.getSibling(path.key).toString(): ', path.getSibling(path.key) + '')
console.log('getSibling: ', path.getSibling(path.key + 1)) // 输出当前节点的下一个节点
console.log('path.getSibling(path.key + 1).toString(): ', path.getSibling(path.key + 1) + '') // 输出当前节点的下一个节点
console.log('path.getSibling(path.key - 1): ', path.getSibling(path.key - 1)) // 输出当前节点的上一个节点
console.log('path.getSibling(path.key - 1).toString(): ', path.getSibling(path.key - 1) + '') // 输出当前节点的上一个节点
console.log('==================path.getPrevSibling====================');
console.log('getPrevSibling: ', path.getPrevSibling() + '') // 获取上一个节点 path.getSibling(path.key - 1)
console.log('getAllPrevSiblings: ', path.getAllPrevSiblings() + '') // 获取前面所有节点
console.log('=================path.getNextSibling======================');
console.log('getNextSibling: ', path.getNextSibling() + '') // 获取下一个节点 path.getSibling(path.key + 1)
console.log('getAllNextSiblings: ', path.getAllNextSiblings() + '') // 获取后续所有节点
path.stop()
}
})
//
scope 详解
scope 提供了一些属性和方法
- 可以方便的查找标识符的作用域
- 获取标识符的所有引用
- 修改标识符的所有引用
- 标识符是否为参数,是否为常量,如果不是常量,也可以知道在哪里修改了它
demo.js
const a = 1000;
let b = 2000;
let obj = {
name: 'xiaojianbang',
add: function(){
a = 400;
b = 300;
let e = 700;
function demo(){
let d = 600;
}
demo();
return a + a + b + 1000 + obj.name
}
}
获取标识符作用域范围 scope.block
//
traverse(ast, {
Identifier(path){
if(path.node.name == 'e'){
console.log('当前遍历到的节点的完整代码: ', path.parentPath.parentPath + '')
// path.scope.block 记录的是当前标识符的作用域范围
console.log(path.scope.block); // 返回的是 node 节点
console.log('e标识符的作用域:\n', generator(path.scope.block).code);
}
}
})
//
可以看到 e 标识符的作用域范围在 function(){} 函数内部
获取函数作用域范围 scope.block 方式获取
对于函数来讲,如果通过 path.scope.block 的方式去获取作用域范围的话,需要从父节点入手
还可以使用 scope 对象下的 getBinding 方法获取
(获取作用域范围基本上都是使用 getBinding 的方式获取)
//
traverse(ast, {
FunctionDeclaration(path){
// 遍历函数声明语句
console.log('当前遍历到的函数名: ', path.node.id.name) // 当前遍历的到是 demo 函数,demo.js 代码中只有 demo 函数为 FunctionDeclaration 节点
// 打印当前节点的作用域范围(对于函数来讲,如果通过 path.scope.block 的方式去获取作用域范围的话,需要从父节点入手)
console.log('path.scope.block: \n', generator(path.scope.block).code);
console.log('==========================================================');
console.log('path.parentPath.scope.block: \n', generator(path.parentPath.scope.block).code);
}
})
//
scope.getBinding()
scope.getBinding() 接收一个类型为 string 的参数,用来获取对应标识符的绑定
如果没有获取到对应标识符的绑定,返回 undefined
//
traverse(ast, {
FunctionDeclaration(path){
// 获取标识符a的绑定
let bindinga = path.scope.getBinding('a')
console.log(generator(bindinga.scope.block).code); // 打印 a 标识符的作用域范围
// 获取 demo 标识符的绑定
let bindingdemo = path.scope.getBinding('demo')
console.log(generator(bindingdemo.scope.block).code); // 打印 demo 标识符的作用域范围
}
})
//
scope.getOwnBinding()
获取当前节点自己的绑定,也就是不包含父级作用域中定义的标识符绑定
但是该函数会得到子函数中定义的标识符绑定
//
function TestOwnBanding(path){
path.traverse({
Identifier(p){
let name = p.node.name
// 判断该标识符的作用域是只是在当前函数
console.log(name, !!p.scope.getOwnBinding(name))
}
})
}
traverse(ast, {
FunctionExpression(path){
// 当前遍历到的是 demo 函数
// 当前遍历到的 FunctionExpression 节点
console.log('当前遍历到的 FunctionExpression 节点\n',
path + ''
)
console.log('===============================')
TestOwnBanding(path)
},
})
//
//
// 该函数会得到子函数中定义的标识符绑定
// 处理得到子函数中定义的标识符绑定
// 过滤掉子函数中定义的标识符的绑定
function TestOwnBinding(path){
path.traverse({
Identifier(p){
let name = p.node.name; // 获取标识符的名字
let binding = p.scope.getBinding(name); // 获取标识符对应的绑定
// 如果绑定存在的话 &&
// 输出标识符的名字, 输出作用域范围是否与path相等
// binding.scope.block 当前标识符的作用域范围
// path + '' 遍历到的节点转 js 代码
binding && console.log(name, generator(binding.scope.block).code == path + '')
/*
当前遍历到的 FunctionExpression 函数表达式
add: function(a){
a = 400;
b = 300;
let e = 700;
function demo(){
let d = 600;
}
demo();
return a + a + b + 1000 + obj.name
}
*/
/*
例如标识符a 标识符a在全局也有一个 const a = 1000;
但是函数内部定义了一个参数 a, 所以a就是局部变量
获取 a 对应的作用域范围,并将获取到的 scope.block 转成js代码
就和当前遍历到的节点相等,代表 a 是当前节点的绑定
*/
}
})
}
traverse(ast, {
FunctionExpression(path){
console.log(path)
TestOwnBinding(path);
}
})
//
referencePaths
referncePaths(记录了标识符被引用的地方)
- 有了 referncePaths 这个对象以后,修改标识符对应的引用就会显得很容易了
- 在还原的时候是会经常用到的
假如标识符被引用,referncePaths 中会存放所有引用该标识符节点的 path 对象
//
traverse(ast, {
FunctionExpression(path){
let binding = path.scope.getBinding('a') // 获取标识符a的绑定
console.log(binding)
}
})
//
//
traverse(ast, {
FunctionExpression(path){
let binding = path.scope.getBinding('a') // 获取标识符a的绑定
console.log(binding.referencePaths)
binding.referencePaths[0].replaceWithSourceString('y + h') // 替换第一个(被引用到的地方)节点
}
})
//
constantViolations
假如标识符有被修改,constantViolations 中会存放所有修改该标识符节点的 path 对象
//
traverse(ast, {
FunctionExpression(path){
let binding = path.scope.getBinding('a') // 获取标识符a的绑定
console.log(binding.constantViolations[0].node)
}
})
//
//
traverse(ast, {
FunctionExpression(path){
let binding = path.scope.getBinding('a') // 获取标识符a的绑定
console.log(binding.constantViolations[0] + ''); // 获取到标识符 a 被修改的地方
console.log(binding.constantViolations[0].node); // 查看对应的 node 节点
// 修改节点,将 400 改成 500
// 1
binding.constantViolations[0].node.right.value = 500
// 2
binding.constantViolations[0].replaceWithSourceString('a=500')
}
})
//
scope.rename(标识符重命名)
scope.rename 可以将标识符重命名
这个方法会同时修改所有引用该标识符的地方
将 add 函数中的 b 变量重命名为 x
//
traverse(ast, {
FunctionExpression(path){
let b = path.scope.getBinding('b') // 获取表示符b的引用
console.log(b.path.node.id.name)
console.log(b.path + '')
b.scope.rename('b', 'x')
}
})
//
scope.generateUidIdentifier()
//
traverse(ast, {
FunctionExpression(path){
// 可以保证标识符永远都不重名
let uid1 = path.scope.generateUidIdentifier('uid') // _uid
console.log(uid1)
let uid2 = path.scope.generateUidIdentifier('uid') // _uid2
console.log(uid2)
let binding = path.scope.getBinding('b')
// generateUidIdentifier 返回的是 Identifier 节点
// 替换的时候需要的是字符串。所以取里面生成的字符串就可以替换了
binding.scope.rename('b', path.scope.generateUidIdentifier("uid").name)
}
})
// //
scope 的其他方法
- hasBinding(‘a’)
a. 查询是否有标识符 a 的绑定,返回 true 或 false
b. 可以用 scope.getBinding(‘a’) 代替
ⅰ. scope.getBinding(‘a’) 返回 undefined 的时候,等同于 scope.hasBinding(‘a’) 返回 fasle - hasOwnBinding(‘a’)
a. 查询是否有自己的绑定,返回 true 或 false
b. 可以用 scope.getOwnBinding(‘a’)代替他
ⅰ. scope.getOwnBinding(‘a’) 返回 undefined 等同于 hasOwnBinding 返回 false - getAllBindings()
a. 获取当前节点的所有绑定,会返回一个对象,
b. 该对象以标识符为属性名,对应的 Binding 为属性值 - hasReference(‘a’)
a. 查询当前节点中是否有 a 标识符的引用,返回 true 或 false - getBindingIdentifier(‘a’)
a. 获取当前节点中的绑定的 a 标识符,返回的是 identifier 的 Node 对象
b. 同样地,这个方法也有 Own 版本 getOwnBindingIdentifier(‘a’)
遍历作用域
- scope.traverse 可以用来遍历作用域
- 可以使用 Path 对象中的 scope
a. 获取函数作用域的时候,需要用到 parentPath - 也可以使用 Binding 中的 scope(推荐使用)
a. 不管是函数还是变量,都能获取到正确的作用域
b. Binding(str).scope 的方式更加的灵活