从Babel开始认识AST抽象语法树

1,767 阅读7分钟

前言

AST抽象语法树想必大家都有听过这个概念,但是不是只停留在听过这个层面呢。其实它对于编程来讲是一个非常重要的概念,当然也包括前端,在很多地方都能看见AST抽象语法树的影子,其中不乏有vue、react、babel、webpack、typeScript、eslint等。简单来说但凡需要编译的地方你基本都能发现AST的存在。

babel是用来将javascript高级语法编译成浏览器能够执行的语法,我们可以从babel出发来了解AST抽象语法树。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~

babel编译流程

了解AST抽象语法树之前我们先来简单了解一下babel的编译流程,以及AST在babel编译过程中起到了什么作用?

1-babel-ast.png

我这里画了张图方便理解babel编译的整个流程

  • parse: 用于将源代码编译成AST抽象语法树
  • transform: 用于对AST抽象语法树进行改造
  • generator: 用于将改造后的AST抽象语法树转换成目标代码

很明显AST抽象语法树在这里充当了一个中间人的身份,作用就是可以通过对AST的操作还达到源代码到目标代码的转换过程,这将会比暴力使用正则匹配要优雅的多。

AST抽象语法树

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST) 是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

虽然在日常业务中我们可能很少会涉及到AST层面,但如果你想在babelwebpack等前端工程化上有所深度,AST将是你深入的基础。

预览AST

说了这么多,那么AST到底长什么样呢?

接下来我们可以通过工具AST Explorer来直观的感受一下!

比如我们如下代码:

let fn = () => {
  console.log('前端南玖')
}

它最终生成的AST是这样的:

2-babel-ast.png

  • AST抽象语法树是源代码语法结构的一种抽象表示
  • 每个包含type属性的数据结构,都是一个AST节点
  • 它以树状的形式表现编程语言的语法结构,每个节点都表示源代码中的一种结构

AST结构

为了统一ECMAScript标准的语法表达。社区中衍生出了ESTree Spec,是目前前端所遵循的一种语法表达标准。

节点类型

类型说明
File文件 (顶层节点包含 Program)
Program整个程序节点 (包含 body 属性代表程序体)
Directive指令 (例如 "use strict")
Comment代码注释
Statement语句 (可独立执行的语句)
Literal字面量 (基本数据类型、复杂数据类型等值类型)
Identifier标识符 (变量名、属性名、函数名、参数名等)
Declaration声明 (变量声明、函数声明、Import、Export 声明等)
Specifier关键字 (ImportSpecifier、ImportDefaultSpecifier、ImportNamespaceSpecifier、ExportSpecifier)
Expression表达式

公共属性

类型说明
typeAST 节点的类型
start记录该节点代码字符串起始下标
end记录该节点代码字符串结束下标
loc内含 line、column 属性,分别记录开始结束的行列号
leadingComments开始的注释
innerComments中间的注释
trailingComments结尾的注释
extra额外信息

AST是如何生成的

一般来讲生成AST抽象语法树都需要javaScript解析器来完成

JavaScript解析器通常可以包含四个组成部分:

  • 词法分析器(Lexical Analyser)
  • 语法解析器(Syntax Parser)
  • 字节码生成器(Bytecode generator)
  • 字节码解释器(Bytecode interpreter)

词法分析

这里主要是对代码字符串进行扫描,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元。

词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token,词法分析过程中不会关心单词与单词之间的关系.

除此之外,还会过滤掉源程序中的注释和空白字符、换行符、空格、制表符等。最终,整个代码将被分割进一个tokens列表

javaScript中常见的token主要有:

关键字:var、let、const等
标识符:没有被引号括起来的连续字符,可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些内置常量
运算符: +、-、 *、/ 等
数字:像十六进制,十进制,八进制以及科学表达式等
字符串:变量的值等
空格:连续的空格,换行,缩进等
注释:行注释或块注释都是一个不可拆分的最小语法单元
标点:大括号、小括号、分号、冒号等

比如我们还是这段代码:

let fn = () => {
  console.log('前端南玖')
}

它在经过词法分析后生成的token是这样的:

工具:esprima

[
    {
        "type": "Keyword",
        "value": "let"
    },
    {
        "type": "Identifier",
        "value": "fn"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Punctuator",
        "value": "("
    },
    {
        "type": "Punctuator",
        "value": ")"
    },
    {
        "type": "Punctuator",
        "value": "=>"
    },
    {
        "type": "Punctuator",
        "value": "{"
    },
    {
        "type": "Identifier",
        "value": "console"
    },
    {
        "type": "Punctuator",
        "value": "."
    },
    {
        "type": "Identifier",
        "value": "log"
    },
    {
        "type": "Punctuator",
        "value": "("
    },
    {
        "type": "String",
        "value": "'前端南玖'"
    },
    {
        "type": "Punctuator",
        "value": ")"
    },
    {
        "type": "Punctuator",
        "value": "}"
    }
]

拆分出来的每个字符都是一个token

语法分析

这个过程也称为解析,是将词法分析产生的token按照某种给定的形式文法转换成AST的过程。也就是把单词组合成句子的过程。在转换过程中会验证语法,语法如果有错的话,会抛出语法错误。

还是上面那段代码,在经过语法分析后生成的AST是这样的:

工具:AST Explorer

{
    "type": "VariableDeclaration",  // 节点类型: 变量声明
    "declarations": [   // 声明
      {
        "type": "VariableDeclarator",  
        "id": {
          "type": "Identifier",  // 标识符
          "name": "fn"  // 变量名
        },
        "init": {
          "type": "ArrowFunctionExpression",    // 箭头函数表达式
          "id": null,
          "generator": false,
          "async": false,
          "params": [],  // 函数参数
          "body": {  // 函数体
            "type": "BlockStatement",  // 语句块
            "body": [   
              {
                "type": "ExpressionStatement",  // 表达式语句
                "expression": {
                  "type": "CallExpression", 
                  "callee": {
                    "type": "MemberExpression",
                    "object": {
                      "type": "Identifier",
                        "identifierName": "console"
                      },
                      "name": "console"
                    },
                    "computed": false,
                    "property": {
                      "type": "Identifier",
                      "name": "log"
                    }
                  },
                  "arguments": [  // 函数参数
                    {
                      "type": "StringLiteral",  // 字符串
                      "extra": {
                        "rawValue": "前端南玖",
                        "raw": "'前端南玖'"
                      },
                      "value": "前端南玖"
                    }
                  ]
                }
            ],
            "directives": []
          }
        }
      }
    ],
    "kind": "let"    // 变量声明类型
  }

在得到AST抽象语法树之后,我们就可以通过改造AST语法树来转换成自己想要生成的目标代码。

常见的解析器

第一个用JavaScript编写的符合EsTree规范的JavaScript的解析器,后续多个编译器都是受它的影响

一个小巧、快速的 JavaScript 解析器,完全用 JavaScript 编写

babel官方的解析器,最初fork于acorn,后来完全走向了自己的道路,从babylon改名之后,其构建的插件体系非常强大

UglifyJS 是一个 JavaScript 解析器、缩小器、压缩器和美化器工具包。

esbuild是用go编写的下一代web打包工具,它拥有目前最快的打包记录和压缩记录,snowpack和vite的也是使用它来做打包工具,为了追求卓越的性能,目前没有将AST进行暴露,也无法修改AST,无法用作解析对应的JavaScript。

AST应用

了解完AST,你会发现我们可以用它做许多复杂的事情,我们先来利用@babel/core简单实现一个移除console的插件来感受一下吧。

这个其实就是找规律,你只要知道console语句在AST上是怎样表现的就能够通过这一特点精确找到所有的console语句并将其移出就好了。

  • 先来看下console语句的AST长什么样 3-babel-ast.png

很明显它是一个表达式节点,所以我们只需要找到name为console的表达式节点删除即可。

  • 编写plugin
const babel  = require("@babel/core")
let originCode = `
    let fn = () => {
        const a = 1
        console.log('前端南玖')
        if(a) {
            console.log(a)
        }else {
            return false
        }
    }
`


let removeConsolePlugin = function() {
    return {
        // 访问器
        visitor: {
            CallExpression(path, state) {
                const { node } = path

                if(node?.callee?.object?.name === 'console') {
                    console.log('找到了console语句')
                    path.parentPath.remove()
                }
            }
        }
    }
}

const options = {
    plugins: [removeConsolePlugin()]
}
let res = babel.transformSync(originCode, options)

console.dir(res.code)

4-babel-ast.png

从执行结果来看,它找到了两个console语句,并且都将它们移除了

这就是对AST的简单应用,学会AST能做的远不止这些像前端大部分比较高级的内容都能看到它的存在。后面会继续更新Babel以及插件的用法。

原文首发地址点这里,欢迎大家关注公众号 「前端南玖」,如果你想进前端交流群一起学习,请点这里

我是南玖,我们下期见!!!