# AST 与 Babel

# 什么是 AST

AST,abstract syntax tree(抽象语法树的缩写),就是代码的语法抽象、结构化的一种表现形式。

JavaScript 中的这么一段代码。

const a = 1

转换成 AST 后可以是下面这种表达形式,可以是 JSON,可以是 JavaScript 中的 object 等树状的数据结构。

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

这种与编程语言无关的抽象、结构化表达,就是 AST

# 身边的 AST

我们也许会在某个社区、某篇文章中看到过 AST 这样的字眼,在不了解它之前,觉得它离我们很“遥远”,但其实 AST 与编程却是息息相关。

# 程序执行

  • 无论是解释型语言还是编译型语言,在真正运行前,它们都有共同的一步,那就是需要将代码解析成结构化的文本,也就是 AST

  • AST 除了让代码结构化,同时也是语义分析的重要组成部分,代码结构化后让编译器可以校验语法是否正确。

  • 最后, AST 会转化成机器所能识别的字节码,让机器按照指令运行起来。

# 编程工具

  • 在前端工程化之中,我们所熟悉的 webpackbabeleslint 甚至编辑器、代码格式化插件等无一不是运作在 AST 之上。

  • AST 的结构化表达让这些工具都能“读懂”代码,从而进行静态分析,实现更多扩展功能。

# AST 的生成步骤

# 词法分析

首先是词法分析(Lexical Analysis),也叫做扫描(scanner)。将我们的代码按照规则转换成一个个 token

如。

const a = 1

词法分析后的表达可以如下。

[
  {
    "type": "Keyword",
    "value": "const"
  },
  {
    "type": "Identifier",
    "value": "a"
  },
  {
    "type": "Punctuator",
    "value": "="
  },
  {
    "type": "Numeric",
    "value": "1"
  }
]

这个过程也可以称为分词,比如这么一句话 “今天天气真好啊。” 能正确理解这句话的前提,就是分词、断句。

那么这句话可以断句为 “今天”,“天气”,“真好”,“啊”,“。”。

# 语法分析

在词法分析之后,就开始语法分析(Syntactic Analysis),也叫做解析(parser)。它会将经过词法分析后得到的 tokens 根据语法规则转换成树的表达形式,也就是 AST,就像文章中的第一段示例代码一样。

这里也可以理解为正确断句(词法分析)之后,才能根据每个词的意思,去理解整个句子的意思。

推荐两个网站,能够在线将代码转换成 tokensAST,方便我们快速验证、加深理解。

  1. https://esprima.org/demo/parse.html (opens new window)
  2. https://astexplorer.net/ (opens new window)

# Babel 基本原理 & AST

AST 有不同的实现标准,比如。

AST 的解析器比较流行的有。

下面就拿比较熟悉的 babel 工具链来进行 AST 的相关操作。

# 将代码解析成 AST

@babel/parser (opens new window)Babel 在使用的 JavaScript 解析器。

安装 @babel/parser,然后新建如下脚本 parse.js

const babelParser = require('@babel/parser')

const code = 'const a = 1'

const ast = babelParser.parse(code)

console.log(ast.program)

执行 node parse.js,可以看出控制台中输入了如下一个对象。这就是 Babel 规范的 AST

Node {
  type: 'Program',
  start: 0,
  end: 11,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 11 },
    filename: undefined,
    identifierName: undefined
  },
  range: undefined,
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  sourceType: 'script',
  interpreter: null,
  body: [
    Node {
      type: 'VariableDeclaration',
      start: 0,
      end: 11,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      declarations: [Array],
      kind: 'const'
    }
  ],
  directives: []
}

上面是直接解析成了 AST,当然其中也有词法分析的过程,只不过这里的 parse 将词法分析 & 语法分析操作结合了,如传入了 tokens 参数,那么输出的结果中就能看见词法分析得到的 tokens 数组,如下。

const babelParser = require('@babel/parser')

const code = 'const a = 1'

const ast = babelParser.parse(code, { tokens: true })

console.log(ast.tokens)

# 访问、更新 AST & AST 转为代码

Babel 中使用 @babel/traverse (opens new window) 来对 AST 进行遍历、更新。

操作完 AST 之后就可以通过 @babel/generator (opens new window),来将 AST 再转换为代码。

安装完 @babel/traverse@babel/generator 后,修改测试脚本。

const babelParser = require('@babel/parser')
const babelTraverse = require('@babel/traverse').default
const babelGenerator = require('@babel/generator').default

const code = 'const a = 1'

// 解析成 AST
const ast = babelParser.parse(code)

// 遍历、更新节点
babelTraverse(ast, {
  enter(path) {
    console.log(path)
  },
  Identifier(path) {
    // 将标识符 a 转换为 b
    if (path.node.name === 'a') {
      path.node.name = 'b'
    }
  }
})

// 重新生成代码
const newCode = babelGenerator(ast, {}, code)

console.log(newCode.code)

上述代码中遍历的 enter 方法,访问每个节点的时候都会执行。而 Identifier 只有访问到标识符时,比如代码里的 a,才会执行。

因为 AST 也是一个对象,所以可以直接修改对象属性值来对 AST 进行修改,比如上面的 path.node.name = 'b'

修改完成之后通过 @babel/generator 将修改过后的 AST 生成代码。可以看见控制台输出了 const b = 1,标识符 a 成功修改为 b

# 总结

经过上面的实践、分析也能了解 Babel 的基本原理了,它是基于 AST,通过下面步骤来实现各种功能。

  1. Parse(解析),将代码解析成 AST,解析阶段又分成以下两个阶段。
    • 词法分析(Lexical Analysis)。
    • 语法分析(Syntactic Analysis)。
  2. Transform(转换),遍历、访问 AST,对其进行修改。
  3. Generate(生成),将修改过后的 AST 转换、生成为代码。

# 参考

  1. Babel Plugin Handbook (opens new window)
  2. How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time (opens new window)
  3. Babel 是如何读懂 JS 代码的 (opens new window)
  4. AST for JavaScript developers (opens new window)
  5. What is JavaScript AST, how to play with it? (opens new window)
上次更新: 4/27/2021, 2:15:49 PM