前言

当你按下"运行"按钮,代码是怎么变成屏幕上的结果的? 你写的每一行代码,计算机其实都"看不懂"——它只认识 0 和 1。编译器就是那个把人类语言翻译成机器语言的"翻译官"。理解编译原理,你就能理解报错信息从哪来、为什么有些语言快有些慢、以及代码优化的底层逻辑。

这篇文章会带你学什么?

学完这章后,你将获得:

  • 全局视野:掌握从源代码到可执行程序的完整编译流水线
  • 词法分析:理解编译器如何把代码拆成一个个 Token
  • 语法分析:理解 AST(抽象语法树)的构建过程
  • AST 可视化:直观看到代码的树形结构
  • 语义分析与优化:理解类型检查和代码优化的原理
  • 优化技术实战:掌握常量折叠、死代码消除等核心优化手段
  • 执行模型:区分编译型、解释型和 JIT 三种执行方式
章节 内容 核心概念
第 1 章 编译器是什么 翻译官类比、编译流水线
第 2 章 词法分析 Token、词法规则
第 3 章 语法分析 AST、语法树、优先级
第 4 章 AST 可视化 交互式语法树、节点类型
第 5 章 语义分析与优化 类型检查、常量折叠、死代码消除
第 6 章 优化技术实战 函数内联、循环外提、常量传播
第 7 章 编译型 vs 解释型 vs JIT 三种执行模型对比

0. 全景图:代码的"翻译之旅"

想象你是一个翻译官,要把一本中文小说翻译成英文。你不会一个字一个字地直译,而是:

  1. 识别词语 — 把句子拆成一个个词(词法分析)
  2. 理解句法 — 判断句子结构是否正确(语法分析)
  3. 理解语义 — 确保意思通顺、没有矛盾(语义分析)
  4. 润色优化 — 让译文更地道流畅(代码优化)
  5. 输出译文 — 写出最终的英文版本(代码生成)

编译器做的事情完全一样,只不过它翻译的是编程语言。


1. 编译器的六步流水线

编译器的工作可以分为六个阶段,像工厂流水线一样,每个阶段处理完交给下一个阶段。

编译流水线
  1. 词法分析(Lexical Analysis):把源代码拆成一个个 Token(单词)
  2. 语法分析(Syntax Analysis):把 Token 组织成语法树(AST)
  3. 语义分析(Semantic Analysis):检查类型是否正确、变量是否声明
  4. 中间代码生成(IR Generation):生成与平台无关的中间表示
  5. 代码优化(Optimization):让中间代码更高效
  6. 代码生成(Code Generation):生成目标平台的机器码
阶段 输入 输出 类比
词法分析 源代码字符流 Token 流 把句子拆成单词
语法分析 Token 流 AST(语法树) 分析句子结构
语义分析 AST 带类型的 AST 检查意思是否通顺
中间代码 带类型的 AST IR 写出初稿
代码优化 IR 优化后的 IR 润色删减
代码生成 优化后的 IR 机器码 输出终稿

2. 词法分析:把代码拆成"单词"

词法分析是编译的第一步。编译器从左到右扫描源代码的每个字符,把它们组合成有意义的Token(词法单元)

就像读英文句子时,你的大脑会自动把字母组合成单词一样,词法分析器把字符组合成 Token:

  源代码: let x = 10 + 5;

Token 流:
[let]   → 关键字(语言保留字)
[x]     → 标识符(变量名)
[=]     → 运算符(赋值)
[10]    → 数字字面量
[+]     → 运算符(加法)
[5]     → 数字字面量
[;]     → 分隔符(语句结束)
  
Token 的五大类型
  • 关键字:语言保留的特殊单词,如 letifreturnfunction
  • 标识符:程序员定义的名字,如变量名、函数名
  • 字面量:直接写在代码里的值,如数字 42、字符串 "hello"
  • 运算符:执行运算的符号,如 +-====
  • 分隔符:分隔代码结构的符号,如 ;,()

3. 语法分析:构建语法树(AST)

词法分析把代码拆成了 Token,但 Token 只是一个个孤立的"单词"。语法分析的任务是把这些 Token 按照语法规则组织成一棵抽象语法树(Abstract Syntax Tree, AST)——它反映了代码的结构和运算优先级。

  表达式: 1 + 2 * 3

语法树:        为什么这样?
       +       因为 * 的优先级
      / \      高于 +,所以
     1   *     2 * 3 先结合
        / \    成为一个子树
       2   3
  
AST 的重要性

AST 是编译器的"核心数据结构",后续的语义分析、优化、代码生成都基于它进行。现代开发工具也大量使用 AST:

  • ESLint:解析代码为 AST,检查是否违反规则
  • Prettier:解析为 AST 后重新格式化输出
  • Babel:解析 AST → 转换 → 生成兼容代码
  • IDE 重构:基于 AST 进行安全的变量重命名、函数提取
语法结构 Token 序列 AST 节点
变量声明 let x = 10 VariableDeclaration → Identifier + Literal
函数调用 add ( 1 , 2 ) CallExpression → Identifier + Arguments
条件语句 if ( a > b ) IfStatement → BinaryExpression + Block

4. AST 可视化:看见代码的"骨架"

上面我们用文字描述了 AST 的结构,但"看到"比"读到"更直观。下面的交互组件让你选择不同的表达式,实时观察它们的语法树长什么样。

通过可视化你会发现,AST 的核心规律其实很简单:

代码结构 AST 根节点 子节点
1 + 2 * 3 BinaryExpression (+) 左: NumericLiteral(1),右: BinaryExpression(*)
let x = 10 VariableDeclaration VariableDeclarator → Identifier(x) + NumericLiteral(10)
add(a, b) CallExpression Identifier(add) + Arguments(a, b)
AST 在日常开发中的应用

你可能没直接写过编译器,但你每天都在用基于 AST 的工具:

  • ESLint / Prettier:解析代码为 AST,检查规则或重新格式化
  • Babel / SWC:解析 AST → 转换语法 → 生成兼容代码
  • IDE 重构:基于 AST 做安全的重命名、提取函数
  • Tree-shaking:分析 AST 中的 import/export,删除未使用的代码

5. 语义分析与代码优化

语法分析确保代码"结构正确",但结构正确不代表"意思正确"。语义分析负责检查代码的含义是否合法,代码优化则让程序跑得更快。

4.1 语义分析:检查"意思"对不对

检查内容 示例 结果
类型检查 int x = "hello" ❌ 类型不匹配
作用域检查 使用未声明的变量 y ❌ 变量不存在
类型推断 1 + 2.0 ✅ 推断结果为 float
参数检查 add(1, 2, 3) 但函数只接受 2 个参数 ❌ 参数数量不匹配
你见过的报错,大多来自语义分析
  • TypeError: Cannot read properties of undefined — 类型检查
  • ReferenceError: x is not defined — 作用域检查
  • Expected 2 arguments, but got 3 — 参数检查

4.2 代码优化:让程序更快

编译器在生成最终代码前,会对中间代码做各种优化。这些优化对程序员透明,但能显著提升性能。

优化技术 优化前 优化后 原理
常量折叠 x = 10 + 5 x = 15 编译时直接算出结果
死代码消除 if (false) { ... } 直接删除 永远不会执行的代码
常量传播 x = 15; y = x * 2 y = 30 已知值直接替换
循环不变量外提 循环内重复计算 len = arr.length 提到循环外 避免重复计算

6. 优化技术实战:编译器如何让代码更快

上面我们提到了几种优化技术的名字,现在来深入看看编译器具体是怎么做的。下面的交互组件展示了 5 种最常见的编译器优化,你可以直观对比优化前后的代码差异。

现代编译器和 JIT 引擎(如 V8、GCC、LLVM)会自动应用数十种优化。作为开发者,你不需要手动做这些优化,但理解它们能帮你:

  • 写出更容易被优化的代码:比如用 const 而不是 let,编译器更容易做常量折叠
  • 理解性能差异:为什么小函数比大函数快?因为编译器能内联它们
  • 避免"反优化":某些写法会阻止编译器优化,比如 eval()with
优化技术 触发条件 性能影响 开发者能做什么
常量折叠 表达式中全是常量 消除运行时计算 多用 const 声明
死代码消除 代码不可达或结果未使用 减小代码体积 及时清理无用代码
循环不变量外提 循环内有不变的计算 减少重复计算 手动提取也是好习惯
函数内联 小函数被频繁调用 消除调用开销 保持函数小而专注
常量传播 变量值在编译时可确定 整条计算链被消除 用常量代替魔法数字

7. 编译型 vs 解释型 vs JIT

代码写完后,有三种"翻译方式"让它运行起来。这三种方式各有优劣,直接决定了语言的性能特征和使用场景。

维度 编译型 解释型 JIT 即时编译
过程 先全量编译成机器码,再执行 边读边执行,逐行翻译 先解释执行,热点代码再编译
运行速度 最快 最慢 中等(热点接近编译型)
启动速度 慢(需要编译) 快(直接运行) 中等(需要预热)
跨平台 需要重新编译 天然跨平台 跨平台
代表语言 C, Rust, Go Python, Ruby JavaScript (V8), Java
为什么 JavaScript 这么快?

V8 引擎的 JIT 编译器会监测哪些代码被频繁执行(热点代码),然后把它们编译成高度优化的机器码。所以虽然 JavaScript 是"解释型语言",但在 V8 中它的性能可以接近编译型语言。这也是 Node.js 能做服务端的底气。


总结

编译原理不是只有编译器开发者才需要了解的知识。理解编译流程,能帮你更好地理解报错信息、选择合适的语言、写出更高效的代码。

回顾本章的关键要点:

  1. 编译器是翻译官:把人类可读的代码翻译成机器可执行的指令
  2. 六步流水线:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
  3. 词法分析拆 Token:把字符流拆成关键字、标识符、运算符等有意义的单元
  4. 语法分析建 AST:按语法规则把 Token 组织成树形结构,反映运算优先级
  5. 语义分析保正确:类型检查、作用域检查,你见过的大多数报错都来自这里
  6. 编译器自动优化:常量折叠、死代码消除、函数内联等技术让代码自动变快
  7. 三种执行模型:编译型最快、解释型最灵活、JIT 兼顾两者

延伸阅读

Last updated 26 Apr 2026, 03:21 +0800 . history