Skip to content
On this page

Path和Node的用法

前言

文本介绍了 Babel 中的 PathNode 的一些用法,在日常工作中应该都会涉及到,所以文章篇幅比较长,建议同学可以边看边动手去尝试调试输出下,会让自己更明白些。

Path 和 Node 的区别

在使用 AST 的过程中,接触最多的就是 pathnode,对于 AST 的初学者来说容易傻傻分不清楚,对于 node 来说就是 path 对象的一个 属性path.node 能取出当前 pathNode 对象,该对象与 AST Explorer 网页中解析出来的 AST 节点结构一致。

简单来说,Node 节点是生成 JS 代码的原材料,是 Path 中的一部分。 Path 是一个对象,用来描述两个节点之间的连接。Path 除了具有上述显示的这些属性以外,还包括 添加更新移动删除 节点等很多有关的方法。

Path 中的方法

Path 中有很多的方法,以下做一些简单的汇总,然后一一做简单举例说明:

  • 获取子节点/Path
  • 判断 Path 类型
  • Path/Node转代码
  • 替换节点属性
  • 替换整个节点
  • 删除节点
  • 插入节点

获取子节点/Path

为了得到 AST 节点的属性值,一般会先访问到该节点,然后利用 path.node.property 方法获取属性,如下代码:

js
const visitor = {  
BinaryExpression(path) {  
console.log(path.node.left);  
console.log(path.node.right);  
console.log(path.node.opeartor);  
}  
};  
  
traverse(ast, visitor);  
  
/*  
Node {type: 'BinaryExpression', ..., left: Node}  
Node {type: 'NumericLiteral', ..., extra: {...}}  
+  
Node {type: 'Identifier', ..., name: 'a'}  
Node {type: 'Identifier', ..., name: 'b'}  
*/  
  

通过这种方法直接 对象.属性 的方法获取到的是 Node 或者具体的属性值, Node 不能使用 Path 相关的方法,如果想要获取到该属性的 Path,就需要使用 Path 对象的 get 的方法,传递的参数为 key(即该属性名的字符串形式)。如果是多级访问,那么就以 进行连接多个 key即可,如下:

js
const visitor = {  
BinaryExpression(path) {  
console.log(path.get('left.name'));  
console.log(path.get('right'));  
console.log(path.get('opeartor'));  
}  
};  
  
traverse(ast, visitor);  
  
/* 会输出 n 个 NodePathNodePath {parent: Node, hub: undefined, contexts: Array(0), data: null, _traverseFlags: 0}  
*/  
  

可以看到,任何形式的属性值,通过 Path 对象的 get 方法去获取,都会被包装成 Path 对象在返回。不过像 operator、name 这一类的属性值没必须要包装成 Path 对象

判断 Path 类型

Path 对象提供相应的方法来判断自身类型,使用方法与 types 组件差不多,只不过 types 组件判断的是 Node 的类型,下面以 a + b + 10 为例子:left是 a + bright是10

js
const visitor = {  
BinaryExpression(path) {  
console.log(path.get('left').isIdentifier());  
console.log(path.get('right').isNumbericLiteral({  
value: 10  
}));  
path.get('left').assertIdentifier();  
}  
};  
  
traverse(ast, visitor);  
  
/* 输出  
false  
true  
报错  
*/  

Path/Node转代码

当代码比较复杂时,遇到问题就需要采用 调试模式 进行调试,比如在 WebStorm 中在行号处点击出现红点后,以调试模式运行就会断到此处,这里跟调试普通的 JS 代码没有区别,适当的可以在某些位置插入一下吐出语句(console.log())来排查错误,在对当前节点排查时,可以把当前的 Path 或者 Node 转换为 JS 代码,来查看下当前处理的节点是否符合自己的预期。可以使用如下代码转换为 JS 代码:

js
const visitor = {  
FunctionExpression(path){  
// Path转换为 JSconsole.log(path.toString());  
// Node转换为 JS,前提是导入了 generator 组件  
console.log(generator(path.node).code);  
}  
}  

替换节点属性

替换节点属性与获取节点属性方法相同,只是改为赋值语句,但是也并非随意替换,需要注意的是,替换的类型要在允许的类型范围内,因此需要熟悉 AST 的结构,如下图所示:

js
/* 原代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return a + b;  
},  
mul: function (a, b) {  
return a + b;  
},  
}  
*/  
  
const visitor = {  
BinaryExpression(path) {  
path.node.left = types.identifier('x');  
path.node.right = types.identifier('y');  
}  
}  
  
traverse(ast, visitor);  
  
/* 替换后的代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return x + y;  
},  
mul: function (a, b) {  
return x + y;  
},  
}  
*/  

替换整个节点

Path 对象中与替换相关的方法有 replaceWithreplaceWithMultiplereplaceInlinereplaceWithSourceString

replaceWith 是用一个节点替换另一个节点,并且是严格的一换一,示例代码如下:

js
/* 原代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return a + b;  
},  
mul: function (a, b) {  
return a + b;  
},  
}  
*/  
  
const visitor = {  
BinaryExpression(path) {  
path.replaceWith(types.stringLiteral('Mankvis'))  
}  
}  
  
traverse(ast, visitor);  
  
/* 替换后的代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return "mankvis";  
},  
mul: function (a, b) {  
return "mankvis";  
},  
}  
*/  

replaceWithMultiple 也是用来替换节点,只不过是多换一,示例代码如下:

js
/* 原代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return a + b;  
},  
mul: function (a, b) {  
return a + b;  
},  
}  
*/  
  
const visitor = {  
ReturnStatement(path) {  
path.replaceWithMultiple([  
types.expressionStatement(types.stringLiteral("mankvis")),  
types.expressionStatement(types.numericLiteral(10)),  
types.returnStatement()  
]);  
}  
}  
  
traverse(ast, visitor);  
  
/* 替换后的代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
"mankvis";  
10;  
return;  
},  
mul: function (a, b) {  
"mankvis";  
10;  
return;  
},  
}  
*/  

上述代码中有两处要特别说明,当表达式语句单独在一行时(没有赋值),最好用 expressionStatement 包裹;替换后的节点,traverse 也是能遍历到的,因此替换时要极其小心,否则容易造成不合理的递归调用。例如上述代码,把 return 语句进行替换,但是替换里面又有 return 语句,就会陷入死循环,解决方法是加入 path.stop,替换完成之后立刻停止遍历当前节点和后续的子节点。

replaceInline,该方法接收一个参数,如果参数不是数组,那么 replaceInline 等同于 replaceWith,如果参数是一个数组,那么 replaceInline 等同于 replaceWithMultiple,其中数组成员必须都是节点。示例代码如下:

js
/* 原代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return a + b;  
},  
mul: function (a, b) {  
return a + b;  
},  
}  
*/  
  
const visitor = {  
ReturnStatement(path) {  
path.replaceInline([  
types.expressionStatement(types.stringLiteral("mankvis")),  
types.expressionStatement(types.numericLiteral(10)),  
types.returnStatement()  
]);  
path.stop();  
}  
}  
  
traverse(ast, visitor);  
  
/* 替换后的代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
"mankvis";  
10;  
return;  
},  
mul: function (a, b) {  
"mankvis";  
10;  
return;  
},  
}  
*/  

上述代码中, visitor 中的函数也加入了 path.stop,原因和之前介绍的一样。

replaceWithSourceString,该方法是用字符串源码替换节点,如把原始代码中的函数改为闭包形式,示例代码如下:

js
/* 原代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return a + b;  
},  
mul: function (a, b) {  
return a + b;  
},  
}  
*/  
  
const visitor = {  
ReturnStatement(path) {  
let argumentPath = path.get('argument');  
argumentPath.replaceWithSourceString(  
'function(){return' + argumentPath + '}()'  
)  
path.stop();  
}  
}  
  
traverse(ast, visitor);  
  
/* 替换后的代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return function(){  
return a + b;  
}  
},  
mul: function (a, b) {  
return function(){  
return a * b;  
}  
},  
}  
*/  

删除节点

在处理混淆代码的时候,在还原代码后,需要删除一些无用的代码,就需要用到删除节点,示例代码如下:

js
traverse(ast, {  
EmptyStatement(path) {  
path.remove();  
}  
})  

EmptyStatemnt 指的是空语句,就是多余的分号,使用 path.remove() 删除当前节点。

插入节点

想要把节点插入到兄弟节点中,可以使用 insertBeforeinsertAfter,分别是在当前节点的前后插入节点,示例代码如下:

js
/* 原代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return a + b;  
},  
mul: function (a, b) {  
return a + b;  
},  
}  
*/  
  
const visitor = {  
ReturnStatement(path) {  
path.insertBefore(types.expressionStatement(types.stringLiteral("Before")));  
path.insertBefore(types.expressionStatement(types.stringLiteral("After")));  
}  
}  
  
traverse(ast, visitor);  
  
/* 替换后的代码  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
return function(){  
"Before";  
return a + b;  
"After";  
}  
},  
mul: function (a, b) {  
return function(){  
"Before";  
return a * b;  
"After";  
}  
},  
}  
*/  

父级 Path 中的方法

以上介绍了 Path 的一些使用方法,接着介绍一些 父级Path 的使用方法。

visitor 处理过程中,使用 console.log() 打印当前 path 节点的时候,会发现有两个属性,分别是 parentPathparent,其中 parentPath 类型为 NodePath,所以它是 父级 Path,而 parent 类型则为 Node,所以它是父节点。只要获取到 父级 Path,那么就可以调用 Path 的各种方法去操作父节点。

父级 Path 的获取可以使用 path.parentPath父级 Path 的 Node 对象可以使用 path.parent 来获取。其实就是说 path.parent = path.parentPath.node,两个效果相同。

  • path.findParent
  • path.find
  • path.getFunctionParent
  • path.getStatementParent

path.findParent

个别情况下,需要从一个路径向上遍历语法树,直到满足相应的条件。这时可以使用 Path 对象的 findParent,示例代码如下:

js
const visitor = {  
ReturnStatement(path) {  
// console.log(path.findParent((p) => p.isObjectExpression()));  
const newPath = path.findParent(function (p){return p.isObjectExpression()})  
console.log(newPath.toString())  
}  
}  
  
traverse(ast, visitor);  

Path 对象的 findParent 接收一个回调函数,在向上遍历每一个 父级 Path 时,会调用该回调函数,并传入对应的 父级 Path,当该回调函数返回真值时,则将对应的 父级 Path 返回。上述代码会遍历 ReturnStatement,然后向上找 父级 Path看,当找到 Path 对象类型为 ObjectExpression 时,就返回该 Path 对象。

path.find

这个方法使用场景比较少,使用方法与 findParent 一致,只不过 find 方法查找的范围包含当前节点,而 findParent 不包含。

js
const visitor = {  
ObjectExpression(path) {  
const findPath = path.find((p) => p.isObjectExpression());  
console.log(findPath);  
}  
}  
  
traverse(ast, visitor);  

path.getFunctionParent

path.getFunctionParent 方法是向上查找与当前节点最接近的 父函数,返回的也是 Path 对象。

path.getStatementParent

path.getStatementParent 方法是向上遍历语法树直到找到语句父节点,例如,声明语句、return 语句、if 语句、switch 语句和 while 语句,返回的也是 Path 对象。该方法从当前节点开始找,如果想要找到 return 语句的父语句,就需要从 parentPath 中去调用,代码如下:

js
const visitor = {  
ReturnStatement(path) {  
const findPath = path.parentPath.getStatementParent();  
console.log(findPath.toString());  
}  
}  
  
traverse(ast, visitor);  

父级 Path 的其他方法

其他方法的使用与之前介绍的 Path 方法类似,如替换父节点 path.parentPath.replaceWith(Node) 和 删除父节点 path.parentPath.remove

同级 Path 中的方法

  • path.inList
  • path.key
  • path.container
  • path.listKey
  • path.getSibling(index)
  • path.getPrevSibling()
  • path.getNextSibling()
  • unshiftContainer()
  • pushContainer()

在介绍 同级 Path 之前,需要先介绍下容器(container),先看下面的例子:

js
traverse(ast, {  
ReturnStatement(path){  
consloe.log(path);  
}  
})  
  
/*  
NodePath{  
parent: Node {...},  
...  
parentPath: NodePath {...},  
...  
container: [  
Node {  
type: 'ReturnStatement',  
...  
argument: [Node]  
}  
],  
listKey: 'body',  
key: 0,  
node: Node {...},  
scope: Scope {...},  
type: 'ReturnStatement'  
}  
*/  

上述代码遍历 ReturnStatemnt 节点,并直接输出 Path 对象,之前介绍的 AST 结构,ReturnStatement 是放在 BlockStatement 中的 body 节点中的,并且该 body 节点是一个数组,输出的 Path 对象中的几个关键属性里,container 就是容器,在这个例子中它是一个数组,里面只有一个 ReturnStatement 节点,与原始代码吻合。 listKey 是容器名,ReturnStatement 是放在 BlockStatementbody 节点中的,因为把 body 节点看作一个容器。

接下来介绍 key,在上述代码输出中的 Path 对象中,可以看到有一个 key 属性,这个 key 就是之前介绍的 path.get 方法的参数。实际上它就是容器对象的属性名,或者说是容器数组的索引。 这里的容器是一个数组,key 代表当前节点在容器中的位置。

并非只有 body 节点才是容器。再来看下面的例子:

js
traverse(ast, {  
ObjectExpression(path) {  
console.log(path);  
}  
})  
  
/*  
NodePath {  
parent: Node {...},  
...  
parentPath: Node {...},  
container: [  
Node {  
type: 'ObjectProperty',  
...  
method: false,  
key: [Node],  
computed: false,  
shorthand: false,  
value: [Node]  
},  
Node {...},  
Node {...}  
],  
listKey: 'properties',  
key: 0,  
node: Node {...},  
scope: Scope {...},  
type: 'ObjectProperty'  
}  
*/  

上述代码遍历 ObjectProperty 节点,然后直接输出 Path 对象, ObjectProperty 是在 ObjectExpressionproperties 属性中的,查看 Path 对象中的几个关键属性,container 是容器,listKey 是容器名。在原始代码中,有三个 ObjectProperty,对应容器中的三个 Node 对象。 key 为 0,表示当前节点是容器中索引为 0 的成员,也就是说容器中的节点互为兄弟(同级)节点。

container并非一直都是数组。例如下面:

js
traverse(ast, {  
ObjectExpression(path) {  
console.log(path);  
}  
})  
  
traverse(ast, visitor);  
  
/*  
NodePath {  
parent: Node {...},  
...  
parentPath: Node {...},  
...  
container: Node {  
type: 'VariableDeclarator',  
...  
id: Node {  
type: 'Identifier',  
...  
name: 'obj'  
},  
init: Node {  
type: 'ObjectExpression',  
...  
properties: [Array],  
extra: [Object]  
}  
},  
listKey: undefined,  
key: 'init',  
node: Node {...},  
scope: Scope {...},  
type: 'ObjectExpression'  
}  
*/  

上述代码中,container 是一个 Node 对象,listKey 为 undefined,其实可以说它没有容器,也就没有兄弟(同级)节点。在原始代码解析后的 AST 结构中,ObjectExpressionVariableDeclarator 的初始化值(init 节点)。此时 key 不是数组下标,而是对象的属性名。

了解了容器之后,接着介绍 同级 Path 相关的属性和方法。一般 container 为数组时就有同级节点,以下内容只考虑 container 为数组的情况,只有这种情况才有意义。示例代码如下:

js
traverse(ast, {  
ReturnStatement(path) {  
console.log(path.inList); // true  
console.log(path.container); // [node {type: 'ReturnStatement' ...}]  
console.log(path.listKey); // body  
console.log(path.key); // 0  
console.log(path.getSibling(path.key)); // node {type: 'ReturnStatement' ...}  
console.log(path.getPrevSibling()); // Array[0] 获取上一个兄弟节点  
console.log(path.getPrevSibling()); // Array[0] 获取下一个兄弟节点  
}  
})  

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 是往容器最后面添加节点。示例如下:

js
traverse(ast, {  
ReturnStatement(path) {  
path.parentPath.unshiftContainer('body', [  
types.expressionStatement(types.stringLiteral('Before1')),  
types.expressionStatement(types.stringLiteral('Before2')),  
]);  
path.parentPath.pushContainer('body', [  
types.expressionStatement(types.stringLiteral('After1')),  
types.expressionStatement(types.stringLiteral('After2')),  
]);  
}  
})  
  
/*  
let obj = {  
name: "mankvis",  
add: function (a, b) {  
"Before1";  
"Before2";  
return a + b;  
"After1";  
"After2";  
},  
mul: function (a, b) {  
"Before1";  
"Before2";  
return a + b;  
"After1";  
"After2";  
}  
};  
*/  

unshiftContainerpushContainer 在 ts 文件中的定义如下:

js
pushContainer<Nodes extends Node | Node []>(listKey: ArrayKeys<T>, nodes: Nodes): NodePaths<Nodes>;  
unshiftContainer<Nodes extends Node | Node []>(listKey: ArrayKeys<T>, nodes: Nodes): NodePaths<Nodes>;  

可以看出,第一个参数给 listKey,第二个参数给 NodesNodesNodes extends Node | Node[],因此可以给 Node 或者 Node数组,最后函数返回加入的 NodesPath 对象。

Released under the MIT License.