babel-traverse 遍历 + Paths(访问者进/出、路径对象)

本节目标

Babel 的遍历比我们手写的 walk 强在哪?三个点:

  1. Visitor 按节点类型分发:你写 Identifier(path) {},Babel 自动在遍历到每个 Identifier 节点时调用它,不用自己 if (node.type === ...)。
  2. enter / exit 两阶段:Identifier: { enter(path) {}, exit(path) {} }。
  3. 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 的“安全修改”这两点吃透,下一节学怎么“造”出新节点就水到渠成了。