抽象语法树AST必知必会

抽象,语法,ast · 浏览次数 : 124

小编点评

**JavaScript 编译器的 AST** AST 是 JavaScript 编译器 AST 的抽象语法树,它是一个树形结构,用于表示 JavaScript 代码的语法结构。 **AST 的基本结构** AST 的基本结构包含以下几个部分: * **节点**:每个 AST 节点表示一个 JavaScript 代码的语法结构。 * **属性**:每个节点拥有一些属性,存储有关该 node 的信息。 * **子节点**:每个节点可以有子节点,表示其代码的结构。 **AST 的常见类型** * **变量**:表示 JavaScript 代码的变量。 * **表达式**:表示 JavaScript 代码的表达。 * **语句**:表示 JavaScript 代码的语句。 * **函数**:表示 JavaScript 代码的函数。 * **表达式树**:表示 JavaScript 代码的表达式树。 **AST 的使用场景** AST 用于各种 JavaScript 代码的处理,例如: * **语法检查**:AST 可以用于语法检查,识别 JavaScript 代码中的语法错误。 * **代码格式化**:AST 可以用于代码格式化,将代码格式化为标准格式。 * **代码高亮**:AST 可以用于代码高亮,突出代码中的语法错误。 * **代码自动补全**:AST 可以用于代码自动补全,完成代码的自动补全。 **AST 的转换** AST 可以通过不同的转换器进行转换,例如: * **esprima**: esprima 是一个 JavaScript AST 解析器,可以用于生成 esprima AST。 * **estraverse**: estraverse 是一个 JavaScript AST travers器,可以用于遍历 AST 并生成新的 AST。 * **escodegen**: escodegen 是一个 JavaScript AST 生成器,可以用于生成新的 AST。

正文

1 介绍 AST

打开前端项目中的 package.json,会发现众多工具已经占据了我们开发日常的各个角落,例如 JavaScript 转译、CSS 预处理、代码压缩、ESLint、Prettier 等。这些工具模块大都不会交付到生产环境中,但它们的存在于我们的开发而言是不可或缺的。

有没有想过这些工具的功能是如何实现的呢?没错,抽象语法树 (Abstract Syntax Tree) 就是上述工具的基石。

Babel,Webpack,Vue-cli 和 EsLint 等很多的工具和库的核心都是通过 Abstract Syntax Tree 抽象语法树这个概念来实现对代码的检查、分析等操作的。在前端当中 AST 的使用场景非常广,比如在 Vue.js 当中,我们在代码中编写的 template 转化成 render function 的过程当中第一步就是解析模版字符串生成 AST。

AST 的官方定义:

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

JS 的许多语法为了给开发者更好的编程体验,并不适合程序的理解。所以需要把源码转化为 AST 来更适合程序分析,浏览器的编译器一般会把源码转化为 AST 来进行进一步的分析来进行其他操作。通过了解 AST 这个概念,对深入了解前端的一些框架和工具是很有帮助的。

那么 AST 是如何生成的?为什么需要 AST ?

了解过编译原理的同学知道计算机想要理解一串源代码需要经过“漫长”的分析过程:

  1. 词法分析 (Lexical Analysis)
  2. 语法分析 (Syntax Analysis)
  3. 代码生成 (Code Generation)

这是在线的 AST 转换器:AST转换器。可以在这个网站上,亲自尝试下转换。点击语句中的词,右边的抽象语法树节点便会被选中,如下图:

代码转化成 AST 后的格式大致如下图所示:

为了方便大家理解抽象语法树,来看看具体的例子。

var tree = 'this is tree'

js 源代码将会被转化成下面的抽象语法树:


{
  "type": "Program",
  "start": 0,
  "end": 25,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 25,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 25,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 8,
            "name": "tree"
          },
          "init": {
            "type": "Literal",
            "start": 11,
            "end": 25,
            "value": "this is tree",
            "raw": "'this is tree'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

可以看到一条语句由若干个词法单元组成。这个词法单元就像 26 个字母。创造出个十几万的单词,通过不同单词的组合又能写出不同内容的文章。

字符串形式的 type 字段表示节点的类型。比如”BlockStatement”,”Identifier”,”BinaryExpression”等。 每一种类型的节点定义了一些属性来描述该节点类型,然后就可以通过这些节点来进行分析其他操作。

2 AST 如何生成

看到这里,你应该已经知道抽象语法树大致长什么样了。那么AST又是如何生成的呢?

以上面var tree = ‘this is tree’为例:

词法分析

其中词法分析阶段扫描输入的源代码字符串,生成一系列的词法单元 (tokens),这些词法单元包括数字,标点符号,运算符等。词法单元之间都是独立的,也即在该阶段我们并不关心每一行代码是通过什么方式组合在一起的。

大致可以看出转换之前源代码的基本构造。

语法分析阶段——老师教会我们每个单词在整个句子上下文中的具体角色和含义。

  • 代码生成

最后是代码生成阶段,该阶段是一个非常自由的环节,可由多个步骤共同组成。在这个阶段我们可以遍历初始的 AST,对其结构进行改造,再将改造后的结构生成对应的代码字符串。

代码生成阶段——我们已经弄清楚每一条句子的语法结构并知道如何写出语法正确的英文句子,通过这个基本结构我们可以把英文句子完美地转换成一个中文句子。

3 AST 的基本结构

抛开具体的编译器和编程语言,在 “AST 的世界”里所有的一切都是节点 (Node),不同类型的节点之间相互嵌套形成一颗完整的树形结构。

{
  "program": {
    "type": "Program",
    "sourceType": "module",
    "body": [
      {
        "type": "FunctionDeclaration",
        "id": {
          "type": "Identifier",
          "name": "foo"
        },
        "params": [
          {
            "type": "Identifier",
            "name": "x"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "body": [
            {
              "type": "IfStatement",
              "test": {
                "type": "BinaryExpression",
                "left": {
                  "type": "Identifier",
                  "name": "x"
                },
                "operator": ">",
                "right": {
                  "type": "NumericLiteral",
                  "value": 10
                }
              }
            }
          ]
        }
        ...
       }
       ...
    ]
}

AST 的结构在不同的语言编译器、不同的编译工具甚至语言的不同版本下是各异的,这里简单介绍一下目前 JavaScript 编译器遵循的通用规范 —— ESTree 中对于 AST 结构的一些基本定义,不同的编译工具都是基于此结构进行了相应的拓展。

4 AST 的应用场景和用法

了解 AST 的概念和具体结构后,你可能不禁会问:AST 有哪些使用场景,怎么用?
代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等

  • 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误。
  • IDE 的错误提示、格式化、高亮、自动补全等等。

代码混淆压缩。

  • UglifyJS2 等。

优化变更代码,改变代码结构使达到想要的结构。

  • 代码打包工具 webpack、rollup 等等。
  • CommonJS、AMD、CMD、UMD 等代码规范之间的转化。
  • CoffeeScript、TypeScript、JSX 等转化为原生 Javascript。

至于如何使用 AST ,归纳起来可以把它的使用操作分为以下几个步骤:

  1. 解析 (Parsing):这个过程由编译器实现,会经过词法分析过程和语法分析过程,从而生成 AST。
  2. 读取/遍历 (Traverse):深度优先遍历 AST ,访问树上各个节点的信息(Node)。
  3. 修改/转换 (Transform):在遍历的过程中可对节点信息进行修改,生成新的 AST。
  4. 输出 (Printing):对初始 AST 进行转换后,根据不同的场景,既可以直接输出新的 AST,也可以转译成新的代码块。

通常情况下使用 AST,我们重点关注步骤2和3,诸如 Babel、ESLint 等工具暴露出来的通用能力都是对初始 AST 进行访问和修改。

这两步的实现基于一种名为访问者模式的设计模式,即定义一个 visitor 对象,在该对象上定义了对各种类型节点的访问方法,这样就可以针对不同的节点做出不同的处理。例如,编写 Babel 插件其实就是在构造一个 visitor 实例来处理各个节点信息,从而生成想要的结果。

const visitor = {

    CallExpression(path) {

        ...

    }

    FunctionDeclaration(path) {

        ...

    }   

    ImportDeclaration(path) {

        ...

    }

    ...

}

traverse(AST, visitor)

5 AST 的转化流程

利用 babel-core (babel 核心库,实现核心的转换引擎) 和 babel-types (可以实现类型判断,生成 AST 节点等)和 AST 来将

let sum = (a, b) => a + b

改成为:

let sum = function(a, b) {
  return a + b
}

实现代码如下:

// babel核心库,实现核心的转换引擎
let babel = require('babel-core');
// 可以实现类型判断,生成AST节点等
let types = require('babel-types');

let code = `let sum = (a, b) => a + b`;
// let sum = function(a, b) {
//   return a + b
// }

// 这个访问者可以对特定类型的节点进行处理
let visitor = {
  ArrowFunctionExpression(path) {
    console.log(path.type);
    let node = path.node;
    let expression = node.body;
    let params = node.params;
    let returnStatement = types.returnStatement(expression);
    let block = types.blockStatement([
        returnStatement
    ]);
    let func = types.functionExpression(null,params, block,false, false);
    path.replaceWith(func);
  }
}

let arrayPlugin = { visitor }
// babel内部会把代码先转成AST, 然后进行遍历
let result = babel.transform(code, {
  plugins: [
    arrayPlugin
  ]
})
console.log(result.code);

分词将整个代码字符串分割成最小语法单元数组,生成 AST 抽象语法树,经过转化 transformer 生成新的 AST 树,遍历生成最终想要的结果 genrator:

AST 的三板斧:

  • 通过 esprima 生成 AST
  • 通过 estraverse 遍历和更新 AST
  • 通过 escodegen 将 AST 重新生成源码

我们可以来做一个简单的例子:
1.先新建一个 test 的工程目录。
2.在 test 工程下安装 esprima、estraverse、escodegen 的 npm 模块

npm i esprima estraverse escodegen --save

3.在目录下面新建一个 test.js 文件,载入以下代码:

const esprima = require('esprima');
let code = 'const a = 1';
const ast = esprima.parseScript(code);
console.log(ast);

将会看到输出结果:

Script {
  type: 'Program',
  body:
   [ VariableDeclaration {
       type: 'VariableDeclaration',
       declarations: [Array],
       kind: 'const' } ],
  sourceType: 'script' }

4.再在 test 文件中,载入以下代码:

const estraverse = require('estraverse');

estraverse.traverse(ast, {
    enter: function (node) {
        node.kind = "var";
    }
});

console.log(ast);

5.最后在 test 文件中,加入以下代码:

const escodegen = require("escodegen");
const transformCode = escodegen.generate(ast)

console.log(transformCode);

输出的结果:

var a = 1;

通过这三板斧:我们将const a = 1转化成了var a = 1

6 实际应用

利用 AST 实现预计算的 Babel 插件,实现代码如下:

// 预计算简单表达式的插件
let code = `const result = 1000 * 60 * 60`;
let babel = require('babel-core');
let types= require('babel-types');

let visitor = {
  BinaryExpression(path) {
    let node = path.node;
    if (!isNaN(node.left.value) && ! isNaN(node.right.value)) {
      let result = eval(node.left.value + node.operator + node.right.value);
      result = types.numericLiteral(result);
      path.replaceWith(result);
      let parentPath = path.parentPath;
      // 如果此表达式的parent也是一个表达式的话,需要递归计算
      if (path.parentPath.node.type == 'BinaryExpression') {
        visitor.BinaryExpression.call(null, path.parentPath)
      }
    }
  }
}

let cal = babel.transform(code, {
  plugins: [
    {visitor}
  ]
});

作者:京东物流 李琼

来源:京东云开发者社区 自猿其说Tech

与抽象语法树AST必知必会相似的内容:

抽象语法树AST必知必会

打开前端项目中的 package.json,会发现众多工具已经占据了我们开发日常的各个角落,它们的存在于我们的开发而言是不可或缺的。有没有想过这些工具的功能是如何实现的呢?没错,抽象语法树 (Abstract Syntax Tree) 就是上述工具的基石。

终于搞懂了!原来 Vue 3 的 generate 是这样生成 render 函数的

前言 在之前的 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令? 文章中讲了transform阶段处理完v-for、v-model等指令后,会生成一棵javascript AST抽象语法树。这篇文章我们来接着讲generate阶段是如何根据这棵javascript AST抽象

Flink测试利器之DataGen初探

Flink SQL 提供了许多扩展功能和语法,以适应 Flink 的流式和批处理引擎的特性。他是Flink最高级别的抽象,可以与 DataStream API 和 DataSet API 无缝集成,利用 Flink 的分布式计算能力和容错机制。

继承,super,重写,多态,抽象,接口

继承,super,重写,多态,抽象,接口 继承 extends 用于表示两个类之间的继承关系,继承是OOP的四大特性之一,他允许一个类(称之为子类或派送类) 继承另一个类(称之为父类或基类)的变量和方法,子类可以复用父类的方法和变量,也可以添加和覆盖父类的方法和变量 extends的基本语法 使用e

spannerlib优雅的go异常处理

蹩脚的go 异常处理 一般写go的人,如果他不是写算法,正常写业务代码的话,可能都会为优雅的异常处理而烦恼,因为脑子抽筋的go设计者们,总是感觉语法糖是一种很低级的东西。但是在我们大多数公司的业务逻辑中,没有语法糖让代码非常丑陋,不易于维护。 如何让go 代码更具有可读性,哪么就要给go加糖! 引入

SICP:元循环求值器(Python实现)

元语言抽象就是建立新的语言。它在工程设计的所有分支中都扮演着重要的角色,在计算机程序设计领域更是特别重要。因为这个领域中,我们不仅可以设计新的语言,还可以通过构造求值器的方式实现这些语言。对某个程序设计语言的求值器(或者解释器)也是一个过程,在应用于这个语言的一个表达式时,它能够执行求值这个表达式所要求的动作。接下来我们将要讨论如何关于在一些语言的基础上构造新的语言。在这篇博客里,我们将用Pyth

C#学习笔记--复杂数据类型、函数和结构体

C#语言的基础知识。在学习练习C#入门知识之后,对C#语言基础的知识进行学习练习! 涉及到语言的基础---一些复杂的数据类型,以及类和结构体。走出简单的小程序代码片段, 开始逐步走向抽象的数据世界。加油!

Go接口 - 构建可扩展Go应用

本文深入探讨了Go语言中接口的概念和实际应用场景。从基础知识如接口的定义和实现,到更复杂的实战应用如解耦与抽象、多态、错误处理、插件架构以及资源管理,文章通过丰富的代码示例和详细的解释,展示了Go接口在软件开发中的强大功能和灵活性。 关注【TechLeadCloud】,分享互联网架构、云服务技术的全

Go有哪些特殊的语言特性?

> 摘要:本文由葡萄城技术团队于博客园原创并首发。转载请注明出处:[葡萄城官网](https://www.grapecity.com.cn/),葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 # 前言 本文主要通过值传递和指针、字符串、数组、切片、集合、面向对象(封装、继承、抽象)和

Llama2-Chinese项目:8-TRL资料整理

TRL(Transformer Reinforcement Learning)是一个使用强化学习来训练Transformer语言模型和Stable Diffusion模型的Python类库工具集,听上去很抽象,但如果说主要是做SFT(Supervised Fine-tuning)、RM(Reward