babel-traverse 遍历 + Paths(访问者进/出、路径对象)
本节目标
- 用 @babel/traverse 遍历真实 JS 的 AST
- 理解 Visitor(访问者) 写法:
Identifier(path) {}就是对 Identifier 节点做事 - 理解 Path(路径):它不只是节点,还带着“节点在哪、父是谁、怎么改”的信息
Babel 的遍历比我们手写的 walk 强在哪?三个点:
- Visitor 按节点类型分发:你写
Identifier(path) {},Babel 自动在遍历到每个Identifier节点时调用它,不用自己if (node.type === ...)。 - enter / exit 两阶段:
Identifier: { enter(path) {}, exit(path) {} }。 - Path 对象:你拿到的
path不是裸节点,而是“节点 + 它的上下文”。通过path你能安全地改树——比如path.replaceWith(...)、path.remove(),Babel 会帮你维护整棵树的连贯性。
心智模型:Path = 节点在树上的“路径 + 身份证”。它知道“我是谁(path.node)、我爸是谁(path.parent)、我从哪个 key 挂在我爸身上(path.key)、我有多少个兄弟(path.container)”。改树只通过 path,别直接改 node,否则树会“对不齐”。
动手:用 babel-traverse 统计并改名
// 运行环境:Node.js 18+,需安装: npm install @babel/parser @babel/traverse
// 运行方式:把整段保存为 app-l6.cjs,终端执行 node app-l6.cjs
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const code = 'const total = price * price + 1'
const ast = parser.parse(code)
// ① 统计各类型节点数量(对比上一节手写的 walk,写法几乎一样)
const counter = {}
traverse(ast, {
enter(path) { counter[path.node.type] = (counter[path.node.type] || 0) + 1 }
})
console.log('节点计数:', counter)
// { Program:1, VariableDeclaration:1, VariableDeclarator:1,
// Identifier:3, NumericLiteral:2, BinaryExpression:2 }
// ② 把所有标识符 price 改名为 unitPrice(注意:通过 path 改,不直接改 node)
traverse(ast, {
Identifier(path) {
if (path.node.name === 'price') path.node.name = 'unitPrice'
}
})
console.log('改名后:', code.replace(/price/g, 'unitPrice'))
// 说明:这里用字符串 replace 仅用于“对照展示”;真实改名请用下面的代码生成(见第 10 节)更完整的 Visitor 写法(enter / exit + 路径信息)
// 运行环境:Node.js 18+,需安装: npm install @babel/parser @babel/traverse
// 运行方式:把整段保存为 app-l6b.cjs,终端执行 node app-l6b.cjs
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const ast = parser.parse('const r = a * b + c')
// 用 enter/exit 两阶段,演示“遍历顺序”与“path.parent 拿父节点”
traverse(ast, {
BinaryExpression: {
enter(path) {
console.log('↓ 进入二元表达式,运算符 =', path.node.operator)
console.log(' 它的父节点类型 =', path.parent.type)
},
exit(path) {
console.log('↑ 离开二元表达式')
}
}
})
// 输出:先进入外层的 +,再进入内层的 *,再依次 exit;
// 每次都能通过 path.parent 拿到“上一层是谁”。名词解释
Visitor(访问者):一种“把‘对不同节点做什么’从‘怎么遍历’里分离出来”的设计模式。你只写 Identifier(path) {},Babel 就会在遍历到每个 Identifier 节点时自动调用它,你不用自己写 if (node.type === 'Identifier')。遍历细节被藏起来了,你只管“遇到某类节点就干什么”。
Path(路径):Babel 遍历时传给你的不是裸节点,而是“节点 + 它的上下文”。通过 path 你能知道“我是谁(path.node)、我爸是谁(path.parent)、我从哪个 key 挂在我爸身上(path.key)、我有多少兄弟(path.container)”。一句话:Path = 节点在树上的“地址 + 身份证”。
课后练习
练习 1:在 app-l6 的统计遍历里,如果想“只数最外层的 BinaryExpression、不算嵌套在它里面的”,该怎么改?
答案:在
BinaryExpression的回调里用path.parent判断——只有path.parent.type !== 'BinaryExpression'时才计数。因为内层+的父节点是外层+,会被这个条件排除掉。
练习 2:教程反复强调“改树要用 path,不要直接改 node”,根本原因是什么?
答案:直接改
node只是改了这一个对象,但树的整体关联(父节点对子节点的引用、作用域信息、Babel 维护的各种元数据)不会同步,path.replaceWith / remove这类 API 则会一并维护这些关联,保证改完树仍然“对得齐”。直接改 node 轻则某些查询失效,重则后续遍历崩溃。
本节小结(观点与完整描述)
本节让你从“能遍历”升级到“会改树”。traverse(ast, { 节点类型(path){} }) 这个 Visitor 写法,核心价值是按类型自动分发——你再也不用手写一堆 if (node.type === ...) 去判断当前节点,Babel 替你路由好了。而真正让你“安全地改”的,是 Path:它不是裸节点,而是带着上下文的“地址”。所有修改都必须通过 path.replaceWith / remove 这类 API 走,Babel 会帮你维护整棵树的连贯性;直接改 node 就像不通知物业就拆墙,房子迟早塌。把 Visitor 的“自动分发”和 Path 的“安全修改”这两点吃透,下一节学怎么“造”出新节点就水到渠成了。