什么是 AST?看一段代码如何变成“树”
本节目标
- 用最直观的方式理解:AST(抽象语法树)到底是什么
- 亲手“看到”一段代码对应的树长什么样
- 记住 AST 节点的统一形状:
{ type, ... }
一句话先记住
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 字段;② “抽象”的意思是丢掉格式、只保留结构。剩下的,交给下一节。