乐趣区

关于前端:从Babel开始认识AST抽象语法树

前言

AST 形象语法树想必大家都有听过这个概念,然而不是只停留在听过这个层面呢。其实它对于编程来讲是一个十分重要的概念,当然也包含前端,在很多中央都能看见 AST 形象语法树的影子,其中不乏有 vue、react、babel、webpack、typeScript、eslint 等。简略来说凡是须要编译的中央你根本都能发现 AST 的存在。

babel是用来将 javascript 高级语法编译成浏览器可能执行的语法,咱们能够从 babel 登程来理解 AST 形象语法树。

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

babel 编译流程

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

我这里画了张图不便了解 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 是这样的:

  • 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 表达式

公共属性

类型 阐明
type AST 节点的类型
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 语法树来转换成本人想要生成的指标代码。

常见的解析器

  • Esprima

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

  • acorn

一个玲珑、疾速的 JavaScript 解析器,齐全用 JavaScript 编写

  • @babel/parser(Babylon)

babel 官网的解析器,最后 fork 于 acorn,起初齐全走向了本人的路线,从 babylon 改名之后,其构建的插件体系十分弱小

  • UglifyJS

UglifyJS 是一个 JavaScript 解析器、放大器、压缩器和丑化器工具包。

  • esbuild

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

AST 利用

理解完 AST,你会发现咱们能够用它做许多简单的事件,咱们先来利用 @babel/core 简略实现一个移除 console 的插件来感受一下吧。

这个其实就是找法则,你只有晓得 console 语句在 AST 上是怎么体现的就可能通过这一特点准确找到所有的 console 语句并将其移出就好了。

  • 先来看下 console 语句的 AST 长什么样

很显著它是一个表达式节点,所以咱们只须要找到 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)

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

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

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

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

退出移动版