什么是 AST?看一段代码如何变成“树”

本节目标

一句话先记住

AST 是源代码语法结构的一张树状地图。编译器/转译器先把你写的代码“翻译”成这张地图,再在地图上做增删改,最后把地图重新画回代码。

先别背定义,我们看一个例子。下面这行 JS:

const answer = 1 + 2

编译器读完它,脑子里会生成这样一棵树(这就是 AST):

VariableDeclaration (const)        // 变量声明
└─ VariableDeclarator              // 一个“变量 = 值”的绑定
   ├─ id: Identifier(answer)       // 变量名 answer
   └─ init: BinaryExpression(+)    // 右边是一个“加法运算”
      ├─ left:  Literal(1)         // 左操作数 1
      └─ right: Literal(2)         // 右操作数 2

发现了吗?AST 里每一个节点都有一个 type 字段(VariableDeclaration、Identifier、BinaryExpression……),其余字段描述这个节点的细节。整棵树就是用一层层 { type, ... } 把“程序在做什么”表达出来。

为什么叫“抽象”:它丢掉了源码里对运行没意义的细节——比如空格、换行、分号、括号的书写形式。它只保留“语法结构”。所以 const answer = 1+2 和 const answer=1 + 2(格式不同)会生成同一棵 AST。

感性认识工具:强烈推荐打开 AST Explorer ,左边粘贴任意代码,右边就是实时生成的 AST。先“看”几百棵树,比看任何文字都管用。

动手:把一颗 AST 打印成树状图

下面这段代码不需要任何依赖,把一颗硬编码的 AST 递归打印成缩进的树。你改改里面的节点,就能直观感受“树”是怎么组织的。

// 运行环境:Node.js 18+(无需安装任何依赖)
// 运行方式:把整段保存为 app-l1.cjs,终端执行  node app-l1.cjs

// 一颗对应 `const answer = 1 + 2` 的极简 AST(结构参考 ESTree 规范)
const ast = {
  type: 'VariableDeclaration',
  kind: 'const',
  declarations: [{
    type: 'VariableDeclarator',
    id: { type: 'Identifier', name: 'answer' },
    init: {
      type: 'BinaryExpression',
      operator: '+',
      left: { type: 'Literal', value: 1 },
      right: { type: 'Literal', value: 2 }
    }
  }]
}

// 递归打印 AST:按深度缩进,把“树”画出来
function printTree(node, depth) {
  if (depth === undefined) depth = 0
  if (!node || typeof node !== 'object' || !node.type) return
  const pad = '  '.repeat(depth)
  // 节点类型 + 最关键的信息(名字/值/运算符/声明方式)
  let info = node.type
  if (node.kind) info += ' (' + node.kind + ')'
  if (node.name) info += ' (name=' + node.name + ')'
  if (node.operator) info += ' (op=' + node.operator + ')'
  if (Object.prototype.hasOwnProperty.call(node, 'value')) info += ' (value=' + node.value + ')'
  console.log(pad + info)
  // 递归处理子节点(数组字段或带 type 的对象字段)
  for (const key of Object.keys(node)) {
    if (['type', 'kind', 'name', 'operator', 'value', 'start', 'end', 'loc'].includes(key)) continue
    const val = node[key]
    if (Array.isArray(val)) val.forEach((child) => printTree(child, depth + 1))
    else if (val && typeof val === 'object') printTree(val, depth + 1)
  }
}

printTree(ast)

// === 终端输出(节选)===
// VariableDeclaration (const)
//   VariableDeclarator
//     Identifier (name=answer)
//     BinaryExpression (op=+)
//       Literal (value=1)
//       Literal (value=2)

名词解释

树(Tree):一种“一个节点可以有多个分支子节点”的数据结构。它只有唯一的根(根没有父节点),末端的叶子节点没有子节点。AST 就是一棵这样的树;你电脑里的文件夹目录、公司的组织架构,本质上也都是树。理解树的关键只有一句话:整体由“节点 + 节点之间的父子关系”组成,而不是一条线。

节点(Node):树里的一个基本单元。在 AST 中,每个节点都带一个 type 字段,用来说明“我是哪一种语法结构”(比如 VariableDeclaration 表示变量声明)。你可以把节点理解为“树上的一个叉点”,它携带了关于这段代码的局部信息。

课后练习

练习 1:下面三行代码,哪些会生成“同一棵” AST?为什么?

const a = 1 + 2
const a = 1 + 2      // 只是多了空格
let   a = 1 + 2      // 只是把 const 换成 let

答案:前两行生成同一棵 AST。因为 AST 是“抽象”的——它丢掉空格、换行等书写格式,只保留语法结构。第三行不同:变量声明方式 kind 从 const 变成了 let,这是语法结构层面的差异,所以树不同。

练习 2:请用对象字面量,写出 let x = 5 对应的极简 AST(只要结构,不用写位置信息)。

答案:
{
type: 'VariableDeclaration', kind: 'let',
declarations: [{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'x' },
init: { type: 'Literal', value: 5 }
}]
}

本节小结(观点与完整描述)

学完本节,你真正要建立的不是一个“AST 的定义”,而是一幅画面:写代码,本质上是在画一棵树。编译器 / 转译器先把你的文字翻译成树,再在树上做增删改,最后把树重新画回文字。这个“文字 ↔ 树”的双向转换,是后面所有工具(Babel、ESLint、TypeScript)共同的地基。你暂时不需要背下每一种节点的名字,只要先牢牢记住两件事就够用:① 每个 AST 节点都有一个 type 字段;② “抽象”的意思是丢掉格式、只保留结构。剩下的,交给下一节。