前端向架构突围系列 - 编译原理 [6 - 1]:AST 抽象语法树与编译器的“三板斧”

55 阅读5分钟

写在前面

你是否好奇过:

  • Babel 是怎么把箭头函数 () => {} 变成 function() {} 的?
  • Prettier 是怎么做到不管你代码写多乱,一保存就变得整整齐齐的?
  • Webpack 是怎么知道 import a from './a' 引用了哪个文件的?

这一切的背后,都没有什么玄学,只有一种数据结构——AST (Abstract Syntax Tree)

对于架构师而言,掌握 AST 意味着你拥有了上帝视角。你不再把代码看作一串死板的字符串,而是看作一棵可以随意修剪、嫁接、重组的树。 本篇我们将揭开编译原理的神秘面纱,让你看清代码的本质。

image.png


一、 什么是 AST?(代码的 X 光片)

在编译器眼中,你写的代码(源码)并不是一段有意义的逻辑,而是一堆单纯的文本字符串。

为了让计算机理解这段文本,我们需要把它转换成一种树状的数据结构,这就叫 AST。

1.1 一个直观的例子

想象一下这句话:

"架构师喝咖啡"

  • 字符串视角: 7 个字符。

  • AST 视角(语法分析):

    • 主语(Subject):架构师
    • 谓语(Verb):喝
    • 宾语(Object):咖啡

同样的,对于 JavaScript 代码:

const a = 1;

在 AST 中,它会被拆解成这样(简化版):

{
  "type": "VariableDeclaration", // 这是一个变量声明语句
  "kind": "const",               // 声明类型是 const
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",    // 变量名是个标识符
        "name": "a"
      },
      "init": {
        "type": "NumericLiteral", // 初始值是个数字字面量
        "value": 1
      }
    }
  ]
}

核心概念: AST 剥离了所有对于机器运行无用的信息(比如空格、换行、注释、括号),只保留了逻辑结构


二、 编译器的“三板斧”:Parse, Transform, Generate

几乎所有的现代前端工具(Babel, ESLint, Prettier, Vue Template Compiler)的工作流程都遵循这三个步骤。我们称之为编译器的三板斧

第一斧:解析 (Parse) —— 从字符串到树

这一步是把“连贯的文本”打散并重组的过程。它又细分为两个子步骤:

  1. 词法分析 (Lexical Analysis / Tokenization):

    • 扫描机 (Scanner) 会逐字读取代码,把代码切成一个个最小的词法单元,叫 Token

    • const a = 1; 会被切成:

      • const (Keyword)
      • a (Identifier)
      • = (Punctuator)
      • 1 (Numeric)
      • ; (Punctuator)
    • 比喻: 就像读英语时,先把字母拼成单词。

  2. 语法分析 (Syntactic Analysis):

    • 解析器 (Parser) 会根据语言的语法规则,把上一步得到的 Tokens 组装成一棵树(AST)。
    • 如果代码写错了(比如 const a = ;),这一步就会报错 SyntaxError
    • 比喻: 就像把单词组装成符合语法的句子结构。

第二斧:转换 (Transform) —— 手术台上的艺术

这是架构师发挥空间最大的一步,也是 Babel 插件、ESLint 规则工作的阶段。

  • 工作方式: 遍历 AST 树的每一个节点(Node)。

  • 操作: 增、删、改、查。

    • 查: 找到所有的 console.log 节点。
    • 删: 把它们从树上移除(生产环境去 log)。
    • 改: 找到所有 const 节点,把 kind 属性改成 var(ES6 转 ES5)。

第三斧:生成 (Generate) —— 从树回归字符串

这是最后一步。

  • 工作方式: 深度优先遍历修改后的 AST,根据节点类型,将其“打印”回普通的 JavaScript 代码字符串。
  • 附加产物: SourceMap。因为代码被改得面目全非了,我们需要 SourceMap 来帮我们映射回源码以便调试。

三、 架构师为什么要学这个?

你可能会问:“我又不去写一个新的编程语言,为什么要学编译原理?” 答案是:工程化能力的降维打击。

3.1 场景一:无痛的代码重构 (Codemod)

需求: 公司的 UI 库从 v1 升级到 v2,组件 <Button size="small"> 必须改成 <Button size="sm">。项目里有 5000 个文件用到了这个组件。

  • 普通开发: 全局搜索替换?万一匹配到了注释里的文字怎么办?万一有的地方写的是 <Button size="small">(多空格)怎么办?只能手动改,耗时一周。
  • 架构师: 写一个 AST 脚本(Codemod)。精准定位到 JSXElement 为 Button 且 prop 为 size 的节点,修改其 value。耗时半天,运行 1 分钟,0 错误。

3.2 场景二:定制化的代码规范 (Custom ESLint)

需求: 团队规定,所有的 HTTP 请求必须包在 try-catch 里,否则不允许提交。

  • 普通开发: Code Review 时肉眼看,经常漏掉。
  • 架构师: 写一个 ESLint 插件。在 AST 中寻找所有调用 axiosfetch 的节点,检查其父节点是否是 TryStatement。如果没有,报错。

3.3 场景三:极致的性能优化

需求: 小程序包体积超标,需要剔除没用到的 CSS 或 JS。

  • 架构师: 利用 AST 分析依赖关系(Tree Shaking 原理),精准识别哪些函数声明了但从未被引用,直接在编译阶段抹除。

四、 神器推荐:AST Explorer

在进入下一节实战之前,你不需要安装任何环境,只需要打开这个网站: astexplorer.net/

这是前端编译领域的“游乐场”。

  1. 语言选择: JavaScript
  2. Parser 选择: @babel/parser
  3. 体验: 在左侧输入 const sum = (a, b) => a + b;,右侧立刻展示出它的 AST 结构。

练手任务: 试着观察一下,一个简单的函数调用 console.log('hello'),在 AST 中是由哪几层包裹起来的?(提示:ExpressionStatement -> CallExpression -> MemberExpression)。


结语:推开新世界的大门

AST 是前端工程化的基石。 掌握了它,你就不仅仅是在写代码,你是在用代码写代码(Metaprogramming)。

现在,我们已经理解了原理,知道了“三板斧”是什么。 下一节,我们将拿起手术刀,真正进入手术室。我们将基于 Babel,亲手写一个插件,把代码里的箭头函数“切”掉,换成普通函数。

Next Step: 理论已备,实战开搞。 请看**《第二篇:实战——掌控代码的手术刀:Babel 插件开发与访问者模式》**。