在线代码转AST语法树网站:AST explorer
什么是path对象
通过以下的代码,对以上图片中的AST语法树做例子。
- VariableDeclarator(path) 是一个函数,表示 traverse 遍历AST时,要进入的节点
- path 参数,表示当前正在遍历的节点路径,path 包含有关节点相关信息的对象。
- 通过 path 对象,可以访问和操作节点的属性和关系,path 对象中又提供提供了很多内置方法供我们使用
- 在代码中的这个例子中,当遍历到一个变量声明 VariableDeclarator 节点时,会调用 VariableDeclarator 函数,并传入该变量声明节点的路径作为参数。在函数中,我们可以通过 path 来访问该节点的信息。在代码中还可以通过 path.toString() 输出当前遍历到的节点的js代码
// todo 编写ast插件 const visitor = { VariableDeclarator(path){ // console.log(path.toString()); // a = 1 console.log(path) } } traverse(ast,visitor)
输出后的path对象:
小结:
- path 对象,表示当前正在遍历的节点路径,path 包含有关节点相关信息的对象。
- path 对象是一个 NodePath 类型
path中的node
在AST中,path 代表的意思是路径的意思,而 node 表示的是节点,node 是被 path 对象包裹的节点,可以通过 path.node 得到当前插件的节点。node是path对象中的一个属性
举例1:在以下的语法树结构中,可以看到 a = 123; 的节点是 VariableDeclarator 来表示这个语句,在代码中可以编写 VariableDeclarator 节点的插件,然后通过 path.node 输出,查看 VariableDeclarator 包含的一些属性
在代码中可以在 visitor 对象中编写,VariableDeclarator节点的插件,然后通过 path.node 输出,可以看到输出的内容是当前 VariableDeclarator 节点中的内容。VariableDeclarator 就是被path对象包裹的一个 node 节点。
// todo 编写ast插件
const visitor = {
VariableDeclarator(path){
console.log(path.node)
/*
Node {
type: 'VariableDeclarator',
start: 4,
end: 11,
loc: SourceLocation {
start: Position { line: 1, column: 4, index: 4 },
end: Position { line: 1, column: 11, index: 11 },
filename: undefined,
identifierName: undefined
},
id: Node {
type: 'Identifier',
start: 4,
end: 5,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: 'a'
},
name: 'a'
},
init: Node {
type: 'NumericLiteral',
start: 8,
end: 11,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
extra: { rawValue: 123, raw: '123' },
value: 123
}
}
* */
}
}
traverse(ast,visitor)
举例2:在以下的语法树结构中,可以看到 a = 123; 的节点是 VariableDeclarator 来表示这个语句,而VariableDeclarator节点中又包含,Identifier 节点和NumericLiteral节点,Identifier节点用于表示 a 变量名,而NumericLiteral表示 变量值 123。这两个节点可以称为 VariableDeclarator 节点中的子节点。
以下的代码中,当 traverse 遍历规则,遍历到 VariableDeclarator 节点时,就会从VariableDeclarator节点,向下遍历到VariableDeclarator节点中的子节点Identifier,然后在Identifier节点中输出 node 节点的属性
const updateParamsNameVisitor = {
Identifier(path){
console.log(path.node)
/*
Node {
type: 'Identifier',
start: 4,
end: 5,
loc: SourceLocation {
start: Position { line: 1, column: 4, index: 4 },
end: Position { line: 1, column: 5, index: 5 },
filename: undefined,
identifierName: 'a'
},
name: 'a'
}
* */
}
}
// todo 编写ast插件
const visitor = {
VariableDeclarator(path){
// {} 为空表示不往遍历到的子节点中传输数据
// {} 在不传输数据的时候也可以忽略
path.traverse(updateParamsNameVisitor,{})
}
}
traverse(ast,visitor)
小结:
- path.node 可以得到当前节点中的node对象,这个对象和AST explorer 解析出来的结构一样
- 通过 path.node 得到当前节点的node对象后,可以从中取出一些属性值
- node 只是 path 对象中的一个属性
- node是一个Node类型
path和node的区别
- path表示当前正在遍历的节点路径,path包含有关节点相关信息的对象。
- 而node是path对象中的一个属性。
- 区别在于 path 是NodePath类型,而node属性是Node类型
- 区别在于 path 得到的是当前节点的对象 ,而node得到的是当前节点的对象
- path对象是当前节点本身,node是当前节点的属性
- path对象是node的父级
- 注意点:
- 通过 path.node.属性名称 得到的属性值不能使用path对象提供的方法 (后面会讲)
path对象中属性和方法
- path.node 获取路径对应的节点
- path.parent 获取当前路径对应节点的父节点
- path.parentPath 获取当前路径对应节点的父路径
- path.scope 表示当前path下的作用域,这个也是写插件经常用到的
- path.container 用于获取当前path下的所有兄弟节点(包括自身)
- path.type 获取当前path的节点类型
- path.get("key") 获取当前path的key值,得到的是一个path对象,能够使用path对象的方法
1. 获取子节点中的属性
通过 path.node 得到的是当前节点中的node对象,node对象中包含一些属性,可以通过 属性名得到node对象中的属性值
代码:通过path.node 得到当前节点的node对象,node对象中又存在着id和init属性,id和init属性也是一个node对象,可以通过node.init的方式访问到init属性,然后取出init属性中对应的值.
const visitor = {
VariableDeclarator(path){
console.log(path.node.init.value) // 1
}
}
traverse(ast,visitor)
需要注意的是,通过这样的方式得到的是属性值或者Node对象的方式,不能使用Path对象提供的方法
2. 获取子path
- 通过 path中的get方法可以得到当前节点中的子级path,从而能够使用path对象的方法
- 提示:如果不理解的话,可以理解为,得到的是node对象,但是能够使用path对象的方法
- 语法格式:path.get("属性名")
- 如果想要多级别访问:path.get("一级属性名.二级属性名")
示例:获取init的path对象
- ast语法树:
- 代码:
// todo 编写ast插件 const visitor = { VariableDeclarator(path){ console.log(path.get("init")) console.log(path.get("id.type")) } } traverse(ast,visitor)
- 输出内容:
3. path和node转代码
path对象和node对象转换为js代码
- path对象转代码:
- path.toString()
- 通过toString() 函数,将遍历的path节点路径,转换为javascript代码
- node对象转代码:
- generator(path.node).code
- 通过generator组件,将node对象转换为javascript代码
示例代码:
// todo 编写ast插件
const visitor = {
VariableDeclarator(path){
console.log(path.toString())
console.log(generator(path.node).code)
}
}
traverse(ast,visitor)
4. 判断path的类型
- path对象提供对应的方法用来判断自身类型,使用方法和types组件差不多
- path对象和types组件判断类型的区别:
- path对象判断的是自身类型
- types组件判断的是node类型
示例:
- 抽象语法树:
- 代码:
// todo 编写ast插件 const visitor = { VariableDeclarator(path){ // 获取当前节点下的init属性path对象,并判断init的节点类型type类型是否为NumericLiteral console.log(path.get("init").isNumericLiteral()); // true } } traverse(ast,visitor)
5. 替换节点属性值
通过types组件将节点中的属性值替换为指定的属性值
示例:将当前VariableDeclarator节点下的init属性的属性值替换为 123123
- 抽象语法树:
- 代码:
// todo 编写ast插件 const visitor = { VariableDeclarator(path){ path.node.init = types.numericLiteral(123123) } } traverse(ast,visitor)
关于valueToNode
在babel中,不同的字面量需要调用不同的方法去生成,当遇到比较多的字面量时,就会很麻烦。
在babel中,为了避免这样的问题,提供了valueToNode方法。会根据传入的数据的数据类型自动转换节点类型
6. 替换整个节点
Path对象中替换相关的方法:
- replaceWith
- replaceWithMultiple
- replaceInline
- replaceWithSourceString
replaceWith,该方法是节点替换节点,并且是严格的一换一,替换的内容是遍历到的节点中的属性值
例如:将变量值全部修改为123321
// todo 编写ast插件
const visitor = {
NumericLiteral(path){
path.replaceWith(valueToNode("123321"))
}
}
/*
* 替换后的代码:
var a = "123321";
var b = "123321";
* */
traverse(ast,visitor)
replaceWithMultiple,该方法是节点替换节点,是多个节点还一个节点。
例如,返回语句是返回一条,但是可以通过replaceWithMultiple方法替换为指定的多条返回语句
例如:将函数内容替换为返回多条语句
// todo 编写ast插件
const visitor = {
ReturnStatement(path){
path.replaceWithMultiple([
types.returnStatement(),
types.expressionStatement(types.stringLiteral("mankvis")),
types.expressionStatement(types.numericLiteral(10)),
]);
path.stop();
}
}
/*
* 替换结果:
function add() {
return;
"mankvis";
10;
}
* */
traverse(ast,visitor)
上述代码中有两处要特别说明:
- 当表达式语句单独在一行时(没有赋值),最好用 expressionStatement 包裹;替换后的节点,traverse 也是能遍历到的,因此替换时要极其小心,否则容易造成不合理的递归调用。
- 例如上述代码,把 return 语句进行替换,但是替换里面又有 return 语句,就会陷入死循环,解决方法是加入 path.stop,替换完成之后立刻停止遍历当前节点和后续的子节点
replaceInline,该方法是replaceWithMultiple和replaceWith的结合,会根据传入的参数决定使用那个方法。
- 如果传入的是一个参数,那么replaceInline就等同于replaceWith
- 如果传入的参数是一个数组,那么replaceInline就等同于replaceWithMultiple
例如:传入的参数是一个参数,将123替换为66666
// todo 编写ast插件
const visitor = {
NumericLiteral(path){
path.replaceInline(valueToNode("66666"))
}
}
/*
* 替换结果:
var a = "66666";
* */
traverse(ast,visitor)
replaceWithSourceString,该方法是用字符串源码替换节点
例如:将123替换为一个函数
// todo 编写ast插件
const visitor = {
NumericLiteral(path){
path.replaceWithSourceString(`function add(a,b){return a + b}`)
}
}
/*
* 替换结果:
var a = function add(a, b) {
return a + b;
};
* */
traverse(ast,visitor)
7. 删除节点
如果想ast遍历到当前某个没有用的节点时候进行删除可以使用,path.remove() 方法
8. 插入节点
插入节点分为两种方式插入:
- path.insertBefore 在节点前插入
- path.insertAfter 在节点后插入
例如:在return语句的前面插入 "Before" 和return 语句后面插入 "After"
// todo 编写ast插件
const visitor = {
ReturnStatement(path){
path.insertBefore(valueToNode("Before"));
path.insertAfter(valueToNode("After"));
}
}
/*
* 替换结果:
function add() {
"Before"
return "hello world";
"After"
}
* */
traverse(ast,visitor)
父级path
将代码 var a = 1; 转换为抽象语法树后,通过以下的代码输出path对象
// todo 编写ast插件
const visitor = {
VariableDeclarator(path){
console.log(path)
}
}
traverse(ast,visitor)
在输出的内容中,可以看到path对象中存在两个属性,分别是parentPath和parent。
- parentPath是NodePath类型,而NodePath中又包含了Node,所以NodePath是父级path
- parent是Node类型,Node是父节点,也就是当前插件的节点。
- 例如:visitor对象中的插件VariableDeclarator,就可以理解为Node父节点
- 所以只要获取了父级path,就可以调用path对象的方法去操作父节点。
- 通过path.parentPath方法可以获取到父级Path,就可以使用path对象提供的方法去对父节点进行一系列的操作
- 父级path的父节点可以通过 path.parent 方法获取
- 父级path的父节点也可以通过 path.parentPath.node 方法获取,因为父节点在父级path中
// todo 编写ast插件 const visitor = { VariableDeclarator(path){ console.log(path.parentPath.node) console.log("=========") console.log(path.parent) } } traverse(ast,visitor)
1. parentPath和parent的关系
- path.parentPath.node 等价于 path.parent,parent是parentPath中的一部分
- 通过以下的代码输出,可以看到得到的是相同的结果,所以印证了上面的话
// todo 编写ast插件 const visitor = { VariableDeclarator(path){ console.log(path.parentPath.node) console.log("=========") console.log(path.parent) } } traverse(ast,visitor)
2. path.findParent()
需要从当前节点向上遍历语法树,直到满足响应的条件,可以使用path对象提供的findParent方法
示例:通过以下代码的输出可以看到,使用了findparent方法是从当前的Identifier节点向上遍历。通过这个方法可以在箭头函数中做一些逻辑判断,是否到达了指定的节点
// todo 编写ast插件
const visitor = {
Identifier(path){
path.findParent((p)=>{
console.log(p.type)
/*
VariableDeclarator
VariableDeclaration
Program
* */
if(p.isVariableDeclarator()){
console.log("到VariableDeclarator节点了")
}
})
}
}
traverse(ast,visitor)
3. path.find()
path.find方法也是从当前节点向上查找,但是包含了当前的节点,可以看到输出的节点类型,包含了当前自身
// todo 编写ast插件
const visitor = {
Identifier(path){
path.find((p)=>{
console.log(p.type)
/*
Identifier
VariableDeclarator
VariableDeclaration
Program
* */
})
}
}
traverse(ast,visitor)
4. path.getFunctionParent
path.getFunctionParent
方法是向上查找与当前节点最接近的 父函数
,返回的也是 Path
对象
5. path.getStatementParent
path.getStatementParent方法是向上遍历语法树直到找到语句父节点。例如,声明语句、return 语句、if 语句、switch 语句和 while 语句,返回的也是 Path
对象。
该方法从当前节点开始找,如果想要找到 return 语句的父语句,就需要从 parentPath
中去调用,代码如下:
const visitor = {
ReturnStatement(path) {
const findPath = path.parentPath.getStatementParent();
console.log(findPath.toString());
}
}
traverse(ast, visitor);
同级path
1. 什么是同级path
- 在AST语法树中,同级path指的是同一层级的节点。
- 每个节点在AST语法树中都有一个唯一的路径,路径是由祖先节点的类型组成
- 同级path表示具有相同祖先节点的节点的路径(path)
例如在以下的图中,左侧定义了两个变量语句,在AST语法树结构的body数组中,存在两个VariableDeclaration祖先节点,这两个VariableDeclaration就是同级path。
同级path不只是这一种,还有被包裹在其他方法里面的。也称为同级path
2. container
container(容器),
3. 同级path种的方法和属性
- path.inList 用于判断是否有同级节点。
- 注意:当container为数组,但只有一个成员时,返回true
- path.key 获取当前节点在容器中的索引
- path.container 获取容器(包含所有同级节点的数组)
- path.listKey 获取容器名
- path.getSibling(index) 用于获取 同级path,其中参数 index 为容器数组中的索引,index通过 path.key 来获取,可以对path.key 进行加减操作来定位到不同的同级path
- path.getPrevSibling 获取当前节点的上一个兄弟节点
- path.getNextSibling() 获取当前节点的下一个兄弟节点
- unshiftContainer 与 pushContainer
unshiftContainer
是往容器最前面添加节点pushContainer
是往容器最后面添加节点
scope作用域
在AST(抽象语法树)中,scope(作用域) 表示一个代码块中变量和函数的可见性和访问权限。它决定了在给定位置访问那些标识符是合法的
scope提供了一些属性和方法,可以方便的查找标识符的作用域:
- 获取标识符的所有引用
- 修改标识符的所有引用
- 判断标识符是否为参数
- 判断标识符是否为常量,如果不是常量,也可以知道从哪里修改它
- 关于什么是标识符?
- 标识符是用于,标识变量、函数、属性和参数的名称
例如,以下的代码中定义了一个 let b = 2000; 的变量,这时候想看变量 b 所有的引用地方在哪里,就可以通过 scope 的 binging(捆绑) 去处理
const a = 1000;
let b = 2000;
let obj = {
name:"AST-0基础菜鸟入门学习",
add:function (a) {
a = 400;
b = 300;
let e = 700;
function demo(){
let d = 600;
}
demo();
return a + a + b + 1000 + this.name;
}
}
在以上的代码中,obj 对象中,编写了一个add函数,在add函数中又定义了一个 demo 函数。这种函数在AST中的类型为:FunctionDeclaration
1. scope.block
scope.block 属性可以获取标识符的作用域,返回的是Node对象。
标识符为变量时:
// todo 编写ast插件
const visitor = {
Identifier(path){
if(path.node.name === "e"){
// console.log(path.scope.block) // 得到的是一个Node对象
// 通过generator将node对象转换为代码
console.log(generator(path.scope.block).code)
/* 输出内容结果
function (a) {
a = 400;
b = 300;
let e = 700;
function demo() {
let d = 600;
}
demo();
return a + a + b + 1000 + this.name;
}
* */
}
}
}
traverse(ast,visitor)
- 代码中,当traverse遍历到节点为Identifier时,并且条件path.node.name === “e” 成立,就将path.scope.block 转换为代码,并输出。
- 由于path.scope.block 得到的是一个Node对象,所以可以使用generator组件转换为代码
- 可以看到以上的代码中输出的是 a 变量所在的作用域的代码。也就是 add 函数
标识符为函数时:
// todo 编写ast插件
const visitor = {
FunctionDeclaration(path){
console.log(generator(path.scope.block).code)
/* 输出结果
function demo() {
let d = 600;
}
* */
}
}
traverse(ast,visitor)
- 以上的代码中,当遍历到节点为FunctionDeclaration时,将path.scope.block得到的Node对象转换为代码并输出。
- 通过输出的结果可以知道,当前demo函数的作用域是 add 范围,但是只输出了 demo 函数,并没有输出 demo 函数所在作用域的代码,遇到这种情况需要获取父级path作用域
// todo 编写ast插件 const visitor = { FunctionDeclaration(path){ console.log(generator(path.scope.parent.block).code) /* 输出结果 function (a) { a = 400; b = 300; let e = 700; function demo() { let d = 600; } demo(); return a + a + b + 1000 + this.name; } * */ } } traverse(ast,visitor)
- 代码中的 path.scope.parent 获取的是当前节点的父节点的作用域,然后在通过 block 获取标识符作用域
2. scope.dump
scope.dump 会得到自己向上的作用域与变量信息,先输出自己当前的作用域,在输出父级作用域,在输出父级的父级的作用域,直到顶级作用域。
// todo 编写ast插件
const visitor = {
FunctionDeclaration(path){
console.log("函数: ",path.node.id.name + "()")
path.scope.dump()
/* 输出结果:
函数: demo()
------------------------------------------------------------
# FunctionDeclaration
- d { constant: true, references: 0, violations: 0, kind: 'let' }
# FunctionExpression
- a { constant: false, references: 2, violations: 1, kind: 'param' }
- e { constant: true, references: 0, violations: 0, kind: 'let' }
- demo { constant: true, references: 1, violations: 0, kind: 'hoisted' }
# Program
- a { constant: true, references: 0, violations: 0, kind: 'const' }
- b { constant: false, references: 1, violations: 1, kind: 'let' }
- obj { constant: true, references: 0, violations: 0, kind: 'let' }
------------------------------------------------------------
* */
}
}
traverse(ast,visitor)
- 可以看到输出的结果:输出了三个作用域
- 输出自己当前的作用域:FunctionDeclaration
- 输出父级作用域:FunctionExpression
- 输出父级的父级作用域:Program
- 输出内容解读:
- # 开头的是每一个作用域,上面输出的内容中一共有3个作用域
- - 开头的是每一个绑定(binding),每一个binding都会包含几个关键信息,分别是:constant,references,violations,kind
- constant:表示是否为常量,true为常量,否false
- references:表示被引用的次数
- kind:表示声明类型
- param 表示参数
- hoistend 提升
- var 变量
- local 内部
- 以上的参数并不是全部,剩下的会在binding对象说明
3. scope.getBinding
scope.getBinding 方法接收一个字符串类型的参数,用来获取对应标识符的绑定(binding)。为了更直观的说明绑定的含义,通过以下代码,遍历FunctionDeclaration,符合要求的就只有demo函数,然后获取当前节点下的绑定a,输出binding。
// todo 编写ast插件
const visitor = {
FunctionDeclaration(path){
let binding = path.scope.getBinding("a")
console.log(binding)
}
}
traverse(ast,visitor)
- 提示:这里获得绑定的 a标识符,是当前demo函数的所在作用域的a。不明白的可以通过generator(path.scope.parent.block).code 输出demo函数当前所在的作用域。
- getBinding中传入的值必须是当前节点能够引用到的标识符名称,如果传入一个不存在的 g ,这个标识符并不存在于当前demo函数的所在的作用域中,或者当前节点不能够引用到 g 标识符。那么getBinding会返回undefined
- 当前FunctionDeclaration节点所在的作用域本身是dem函数,为什么能找到a呢?因为demo本身的作用于是add这个函数,所以能找到 a 标识符
- 输出结果:
- constant:表示是否为常量,true为常量,否false
- references:表示被引用的次数
- kind:表示声明类型
- param 表示函数参数
- hoistend 提升
- var 变量
- local 内部
- 注意:以上代码中,a是add函数的参数,当函数中局部变量和全局变量重名时,使用的是局部变量
- identifier:是a标识符的Node对象(节点)
- path 是 a 标识符的Path对象
- referencePaths 标识符如果被引用,那么referencePaths中会存放所有该标识符的节点的path对象
- constantViolations 标识符如果被修改,那么constantViolations中会存放所有修改该标识符的节点的path对象
在使用Binging方法的时候,返回的是一个binding对象,binding对象中也有scope。以上的代码中因为获取的是a的binging,所以是a的scope。
通过以下的代码,加入获得是demo的binding对象和a的binding对象,将其转换为代码,输出的是相同的作用域,add函数。
traverse(ast, {
FunctionExpression(path) {
let bindingA = path.scope.getBinding('a');
let bindingDemo = path.scope.getBinding('demo');
console.log(bindingA.referenced);
console.log(bindingA.references);
console.log(generator(bindingA.scope.block).code);
console.log(generator(bindingDemo.scope.block).code);
},
})
/*
true
2
下面代码输出2次
function (a) {
a = 400;
b = 300;
let e = 700;
function demo() {
let d = 600;
}
demo();
return a + a + b + 1000 + obj.name;
}
*/
4. scope.getOwnBinding
scope.getOwnBinding 该函数用于获取当前节点自己的binding(绑定),也就是不包含父级作用域中定义的标识符。
以下是获取当前demo函数作用域中的标识符 d 的 binding(绑定)
// todo 编写ast插件
const visitor = {
FunctionDeclaration(path){
let binding = path.scope.getOwnBinding("d")
console.log(binding)
}
}
traverse(ast,visitor)
但是getOwnBinding这个方法有一个缺点,就是会获取子函数中定义的标识符。
let TestOwnBinding = (path)=>{
path.traverse({
Identifier(path){
let name = path.node.name;
console.log(`标识符: ${name} ${!!path.scope.getOwnBinding(name)}`)
}
})
}
// todo 编写ast插件
const visitor = {
FunctionExpression(path){
TestOwnBinding(path)
}
}
/* 输出结果
标识符: a true
标识符: a true
标识符: b false
标识符: e true
标识符: demo false
标识符: d true // 子函数demo中的标识符 d
标识符: demo true
标识符: a true
标识符: a true
标识符: b false
标识符: name false
* */
traverse(ast,visitor)
- 上述代码中,当traverse遍历到节点为FunctionExpress(FunctionExpress节点也就是add函数),
- 然后遍历该函数下的所有Identifier,输出标识符名称,通过getOwnBinding方法得到的绑定结果。
- 通过输出结果可以看到,子函数demo中的标识符 d,也可以被getOwnBinding方法得到,也就是说如果想获取当前节点下定义的标识符,而不涉及子函数中的标识符的话,需要进一步的判断,可以通过判断标识符的作用域是否与当前函数作用域来确定
getBinding获取当前作用域下的标识符而不是涉及子函数中的标识符
let TestOwnBinding = (path)=>{
path.traverse({
Identifier(p){
let name = p.node.name;
let binding = p.scope.getBinding(name)
// console.log(!!binding)
// console.log(generator(binding.scope.block).code)
binding && console.log(`标识符: ${name} `, generator(binding.scope.block).code === path.toString())
}
})
}
// todo 编写ast插件
const visitor = {
FunctionExpression(path){
TestOwnBinding(path)
}
}
/* 输出结果, 结果为true的就是当前add函数作用域中的标识符,false都是子函数中的标识符
标识符: a true
标识符: a true
标识符: b false
标识符: e true
标识符: demo true
标识符: d false
标识符: demo true
标识符: a true
标识符: a true
标识符: b false
* */
traverse(ast,visitor)
通过以上的代码,得到了当前add函数作用域中的标识符,而不会涉及到子函数demo中的标识符
5. scope.traverse
scope.traverse方法用于遍历当前作用域中的节点。可以使用path对象中的scope遍历path.scope.traverse,也可以使用binding中的scope遍历binding.scope.traverse,更推荐使用binding.scope.traverse
// todo 编写ast插件
traverse(ast, {
FunctionDeclaration(path) {
let binding = path.scope.getBinding('d');
// binding.scope.block 表示在遍历时,在当前作用域中遍历
binding.scope.traverse(binding.scope.block, {
VariableDeclarator(p) {
if (p.node.id.name === "d"){
p.node.init = types.numericLiteral(10000100001000010000)
}
},
})
}
})
6. scope.rename
使用scope.rename 可以将标识符进行重命名,这个方法会同时修改所有引用该标识符的地方,例如将add函数中的变量重命名为 bbbbbbbb
// todo 编写ast插件
traverse(ast, {
FunctionExpression(path) {
// 首先要拿到b标识符的绑定
let binding = path.scope.getBinding("b")
binding.scope.rename("b","bbbbbbbbbbbbbbb")
}
})
/*修改后的结果
const a = 1000;
let bbbbbbbbbbbbbbb = 2000;
let obj = {
name: "AST-0基础菜鸟入门学习",
add: function (a) {
a = 400;
bbbbbbbbbbbbbbb = 300;
let e = 700;
function demo() {
let d = 600;
}
demo();
return a + a + bbbbbbbbbbbbbbb + 1000 + this.name;
}
};
* */
7. scope.hashBinding
scope.hashBinding方法用于查询某个标识符是否有绑定,返回true和false。这个方法也可以用scope.getBinding方法代替,因为getBinding如果找不到标识符会返回undefined,找到的话返回一个binding对象
8. scope.hashOwnBinding
scope.hashOwnBinding方法用于查询当前节点中是否有自己的绑定,返回true和false。可以使用scope.getOwnBinding来代替,因为都是查询自身作用域中是否存在绑定,而getOwnBinding查询到有绑定的话会返回一个binding对象,否则undefined
9. scope.getAllBindings
scope.getAllBindings 方法用于查询当前节点的所有绑定。返回的对象是以标识符名为属性为,对用的Binding为属性值
// todo 编写ast插件
traverse(ast, {
FunctionExpression(path) {
console.log(path.scope.getAllBindings())
}
})
/*输出结果
a = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "param", constantViolations: Array(1), ...}
e = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "let", constantViolations: Array(0), ...}
demo = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "hoisted", constantViolations: Array(0), ...}
b = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "let", constantViolations: Array(1), ...}
obj = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "let", constantViolations: Array(0), ...}
* */
遍历通过bindings属性得到的所有绑定
// todo 编写ast插件
traverse(ast, {
BlockStatement(path) {
console.log('\nBlockStatement节点源码: \n', path.toString());
let bindings = path.scope.bindings;
console.log('作用域内被绑定数量:', Object.keys(bindings).length);
for (const bindingsKey in bindings) {
console.log('名字', bindingsKey);
let binding_ = bindings[bindingsKey];
console.log('类型:', binding_.kind);
console.log('定义:', binding_.identifier);
console.log('是否常量:', binding_.constant);
console.log('被修改信息记录:', binding_.constantViolations);
console.log('是否被引用:', binding_.referenced);
console.log('被引用次数:', binding_.references);
console.log('被引用信息NodePath记录', binding_.referencePaths);
}
console.log('-------------------------------------');
},
})
10. scope.hasReference
scope.hasReference("a") 表示查询当前节点中是否有a标识符的引用,返回布尔值
11. scope.getBindingIdentifier
scope.getBindingIdentifier("a") 表示获取当前节点中绑定a的标识符,返回Identifier的Node对象。
总结:
在Babel中,抽象语法树(AST)中的几个重要概念包括:path对象、node对象、scope对象和binding对象。
1. path对象:path对象表示AST中的一个节点,并提供了对该节点进行操作的方法。每个path对象都与一个具体的node对象相关联。通过path对象可以访问并修改AST的结构和属性。它提供了一系列方法,比如`node()`, `parent()`, `siblings()`, `insertBefore()`等,可以用于获取、修改和操作AST的节点。
2. node对象:node对象是AST中的一个节点,代表了JavaScript代码中的某个语法结构(如函数、变量声明、表达式等)。每个node对象都包含了该语法结构的类型和相关的属性。Babel使用不同的node对象来表示不同的语法结构,比如`FunctionDeclaration`表示函数声明,`VariableDeclaration`表示变量声明等。
3. scope对象:scope对象表示一个代码块(如函数或块级作用域)中的变量和函数的作用域。每个scope对象都有一个或多个binding对象,代表了该作用域中的变量和函数。通过scope对象可以查找和管理作用域中的标识符。
4. binding对象:binding对象表示一个变量或函数的引用。每个binding对象都与一个具体的标识符相关联,并包含了该标识符在作用域中的绑定信息,比如变量的名称、作用域、声明的位置等。通过binding对象可以获取标识符的绑定信息,比如变量的值、类型等。
综上所述,path对象用于操作AST中的节点,node对象表示AST中的语法结构,scope对象表示代码块的作用域,binding对象表示变量或函数的引用。它们共同组成了Babel中对JavaScript代码进行解析和转换的基础。