JavaScript中的抽象语法树与实际应用 (Abstract Syntax Trees and Practical Applications in JavaScript )
抽象语法树(AST)乍一听像是计算机科学里晦涩难懂的专业术语,但只要掌握基础概念,就会发现它其实很好理解。本文将带你轻松入门抽象语法树,并讲解其在 JavaScript 中的实际应用场景。
如果你想搞懂 AST(抽象语法树)的基础知识以及它的实际应用,那这篇文章就是为你准备的。我们不会预设你具备任何 AST 的相关背景,而是会采用最直截了当的方式来为你讲解这些概念。
这篇文章不会带你深究程序在运行前经历的各种复杂阶段,而是专注于帮你加深对 AST(抽象语法树)的理解,并展示它在你的 JavaScript 开发之旅中有哪些实际应用。为了达到这个目的,我们将深入探讨那些高度依赖 AST 的实用工具。
为了能顺畅地跟上文章的节奏,你需要具备基础的 JavaScript 知识。在本文的后半部分,我们将探索各种 JavaScript 工具,并一起动手写代码实操。
免责声明:如果你已经开发过 Babel 或 ESLint 的插件,那么这篇文章对你的帮助可能有限,因为你大概率已经非常熟悉这里讲的大部分知识了。
什么是AST?
抽象语法树(AST)是一种层级数据结构,在计算机科学和编程语言理论中,它被用来表示源代码或编程语言中表达式的语法结构。它通常作为代码在编译或解释过程中的中间表示形式。
这一大段话听起来是不是有点头大?咱们把它简单化。
你写下的每一段源代码,无论最终是为了被解释还是编译,都会经历一个叫做‘解析’的过程。在这个过程中,代码会被转换成一棵抽象语法树(AST),它就像是代码底层结构的一种层级化、结构化的表现形式。
既然搞明白了这一点,咱们就来看一段实际的代码,以及它对应的 AST 长什么样:

直接访问https://astexplorer.net/#/gist/e2b13cfd7074c4e5fc2afed54cdb6e3a/6e8dab825553c0e738013826f03129e4a416cf76 会看得更加清楚!!!
看右侧的 AST,你会发现它呈现出一种树状结构。我们从代表整个文件的根节点(类型为 Module)开始,它里面包含了一个
body 属性,这个 body 里又装着其他节点,比如 ImportDeclaration(导入声明)、VariableDeclaration(变量声明)和 VariableDeclarator(变量声明器)。这样一来,代码的每个部分就被描述得清清楚楚了。请注意,如果你使用不同的解析器,生成的 AST 可能会有些细微的差别,但核心思路是完全一样的——它就是一种用来表示源代码的树状结构。还记得我们之前提过的吗?每段源代码在被编译或解释之前,都会先被解析成 AST。举个例子,像 Node.js 和基于 Chromium 的浏览器,它们背后都是靠 Google 的 V8 引擎来运行 JavaScript 的。当然,在解释器真正开始工作之前,肯定也少不了 AST 解析这一步。我专门去翻了翻 V8 的源码,发现它正是通过自己内部的解析器来实现这一点的。
既然 JavaScript 引擎都已经自带了内部解析器,那我们为什么还需要 Babel parser、SWC parser、Acorn、Espree 等其他 JavaScript 解析器呢?
它们的存在,是为了给其他工具提供一个基础(或者说是一个通用的起点)。举个例子,像代码转译器(Transpilers)、代码压缩器(Minifiers)、代码检查工具(Linters)、代码修改工具(Codemods)、语言处理器以及代码混淆器等等,它们在幕后都会使用解析器,先把你的代码解析成 AST,然后才能进行各种转换,或者执行任何形式的分析。
说到抽象语法树(AST)的实际应用,本文将重点探讨两个最主流的用途:转译器(Transpilers)和代码检查工具(Linters),特别是结合 JavaScript 语境来聊聊它们。
在这里,我们将看到 AST 在这些应用中扮演着怎样至关重要的角色,它让开发者能够高效地转换和分析代码。
代码转译

转译器(Transpiler),也就是‘源码到源码的编译器’的简称,是一种软件工具,它负责把用一种编程语言写成的源代码,翻译成另一种编程语言(或者同一语言的不同版本)的等效代码。转译器通常用于实现语言兼容、语法转换以及代码优化等多种目的。
其中,语法转换是一个非常常见的场景,尤其是在处理兼容性问题时。想象一下,假设我们开发了一个应用,而部分用户还在使用一些老旧的浏览器。如果我们采用了像空值合并运算符(??)这样的新语法特性,就可能会导致这部分用户根本无法使用我们的应用。为了解决这个问题,我们必须在将代码部署到生产环境之前,把它转换成老旧浏览器能够兼容的旧语法。这样一来,就能确保使用旧版浏览器的用户依然可以正常访问和使用我们的应用。
正如上图所示,转译器首先会将代码解析成一棵抽象语法树(AST)。紧接着,它会根据需要去转换(修改)这棵 AST,最后再基于修改后的 AST 重新生成新的代码。
Babel 是前端生态系统中非常常见的一款 JavaScript 转译器,你可能直接或间接地使用过它。在接下来的内容里,我们会详细聊聊它到底是如何运用 AST 的。
代码检查
另一个非常依赖 AST 的软件工具就是代码检查器(Linters)。Linters 会自动分析并检查源代码,找出其中潜在的错误、风格违规以及是否符合编程最佳实践,从而帮助开发者在开发过程中及时发现并修正代码里的问题。
在 Linter 对你的源代码进行静态分析之前,它也会先把代码解析成一棵抽象语法树(AST)。一旦解析完成,Linters 就会接着去遍历这棵 AST,以此来识别并处理代码中潜在的问题。
ESLint 是 JavaScript 社区里应用最广泛的代码检查器。它拥有强大的插件系统、海量的插件库、丰富的编辑器扩展以及预设规则集(也就是打包好的一组插件),你可以非常轻松地把它们集成到你的项目中。稍后在这篇文章里,我们也会详细聊聊 ESLint。
转译器中的 AST(Babel)
既然你已经了解了 AST 的几个实际应用案例,接下来我们将深入探讨转译器。具体来说,我们会以 Babel 为例,并动手写一个插件。
Babel 为我们提供了一整套转译代码的工具链,它包含了命令行界面(CLI)、解析器以及一套插件系统。这意味着你可以编写一个插件来对你的代码进行特定的转换,甚至还可以把它发布到 npm 上,让任何人都能安装和使用。
代码转译并不是 JavaScript 独有的,你也可以使用像 PostCSS 这样的工具,为你的 CSS 源代码增加一层转换处理。大多数拥有相当成熟生态系统的编程语言,大概率都会有类似的工具来帮助进行代码转换。
Babel 会读取你的每一个文件,根据代码生成一棵抽象语法树(AST),然后将这棵 AST 连同额外的信息一起传递给插件。插件随后就可以对 AST 应用所需的转换。转换完成后,最终生成的 AST 会被重新转换回代码。这里需要特别注意的是:如果没有插件,Babel 实际上什么也不会做。你输入的是什么代码,输出的依然会是完全相同的代码。
为了让我们对 AST 的理解更加实战化,接下来我们将动手写一个简单的 Babel 插件,用来移除代码中的
console.log。毕竟,大多数 JavaScript 开发者在调试代码时,都难免会随手到处写 console.log。而我们的这个插件,就能把源代码里的 console.log 彻底清除干净。(不过顺便提一句,在大多数情况下,你其实更希望在把代码提交到仓库之前,就通过代码检查工具(Linter)来发现并拦截这些
console.log,而不是单纯等到构建阶段再把它们移除掉。) 克隆模板
你可能已经注意到了,这并不是一篇手把手教你‘如何从零开始写 Babel 插件’的全面教程,所以我们不会花太多时间去讲怎么创建插件。相反,我们会直接从一个我专门为这篇文章设计的模板开始入手。
这个模板托管在一个单体仓库(monorepo)里,这样能方便我们在同一个项目下管理多个不同的包。在模板的
plugin 目录下,已经包含了两个插件,一个是 Babel 插件,另一个是 Eslint 插件。你可以在 GitHub 上访问这个仓库。现在,让我们先从克隆这个仓库开始吧:
https://github.com/marvinjude/ast-and-practical-js-applications.git
切换到 starter 分支:
git checkout starter
这个仓库包含两个分支:
main 和 starter。其中,starter 分支是一个空白的模板,我们会在这篇文章中逐步在上面进行开发和补充。而另一方面,main 分支则记录了我们在这个 starter 分支上所做的每一次修改(相当于一个完整的参考答案)。如果在开发过程中有需要,你可以随时将 main 分支与你正在进行的更新进行对照参考。
浙公网安备 33010602011771号