抽象语法树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转换器。可以在这个网站上,亲自尝试下转换。点击语句中的词,右边的抽象语法树节点便会被选中,如下图:

抽象语法树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必知必会

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

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

  • 代码生成

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

抽象语法树AST必知必会

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

3 AST 的基本结构

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

抽象语法树AST必知必会

{
  "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 结构的一些基本定义,不同的编译工具都是基于此结构进行了相应的拓展。

抽象语法树AST必知必会

4 AST 的应用场景和用法

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

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

代码混淆压缩。

  • UglifyJS2 等。

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

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

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

抽象语法树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必知必会

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文章来源地址https://www.toymoban.com/news/detail-594352.html

到了这里,关于抽象语法树AST必知必会的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 聊聊Flink必知必会(七)

    虽然数据流中的许多操作一次只查看一个单独的事件(例如事件解析器),但某些操作会记住多个事件的信息(例如窗口算子)。 这些操作称为有状态的(stateful)。 有状态操作的一些示例: 当应用程序搜索某些事件模式(event patterns)时,状态(state)将存储迄今为止遇到的事件序

    2024年02月04日
    浏览(47)
  • MySQL必知必会(初级篇)

    数据库 (DataBase,DB),是统一管理的、长期存储在计算机内的、有组织的相关数据的集合。特点是数据见联系密切、冗余度小、独立性高、易扩展,并且可以为各类用户共享。 MySQL :是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的

    2023年04月08日
    浏览(47)
  • 聊聊Flink必知必会(五)

    聊聊Flink的必知必会(三) 聊聊Flink必知必会(四) 从源码中,根据关键的代码,梳理一下Flink中的时间与窗口实现逻辑。 对数据流执行 keyBy() 操作后,再调用 window() 方法,就会返回 WindowedStream ,表示分区后又加窗的数据流。如果数据流没有经过分区,直接调用 window() 方法则会返

    2024年02月05日
    浏览(62)
  • 聊聊Flink必知必会(二)

    Flink是一个有状态的流处理框架,因此需要对状态做持久化,Flink定期保存状态数据到存储空间上,故障发生后从之前的备份中恢复,这个过程被称为Checkpoint机制。而Checkpoint为Flink提供了Exactly-Once的投递保障。 流处理是一个数据不断输入的过程,为了更好更方便的快照,需要

    2024年02月08日
    浏览(45)
  • ChatGPT入门必知必会

    更多文章欢迎关注公众号: stackoveriow 2023年是真正意义上的AI之年,因为ChatGPT 2007年,iPhone开启了智能手机时代, 2023年,我们迎来了人工智能时代,我们正处于历史的大转折点上,这也许是启蒙运动级别的思想和社会转折,工业革命级别的生产和生活转折 。继22年12月份从GP

    2023年04月18日
    浏览(101)
  • 【数据库】索引必知必会

    数据库中索引(Index)是一种帮助快速查找数据的数据结构,可以把它理解为书的目录,通过索引能够快速找到数据所在位置。 使用索引可以加快数据查找的效率,这是创建索引的最主要原因。 场景的索引数据结构有:Hash表(通过hash算法快速定位数据,但不适合范围查询,

    2023年04月20日
    浏览(48)
  • 必知必会Java命令-jps

    你好,我是阿光。 最近想着把工作中使用过的java命令都梳理一下,方便日后查阅。虽然这类文章很多,但自己梳理总结后,还是会有一些新的收获。这也是这篇笔记的由来。 今天先聊聊 jps 命令。 jps 命令是JDK提供的一个工具,用于查看目标系统上的Java进程基本信息(进程

    2024年02月05日
    浏览(39)
  • 《MySQL 必知必会》课程笔记(三)

    创建和修改数据表,是数据存储过程中的重要⼀环。 我们不仅需要把表创建出来,还需要正确地设置限定条件,这样才能确保数据的一致性和完整性。 同时,表中的数据会随着业务需求的变化而变化,添加和修改相应的字段也是常见的操作。 首先,我们要知道 MySQL 创建表的

    2024年02月03日
    浏览(41)
  • 【SQL 必知必会】- 第九课 汇总数据

    目录 聚集函数          AVG()函数         只用于单个列         COUNT()函数         COUNT()函数有两种使用方式:         MAX()函数         对非数值数据使用MAX()         MIN()函数         对非数值数据使用MIN()         SUM()函数         在多

    2023年04月09日
    浏览(51)
  • Python必知必会 os 模块详解

    ❤️ 作者简介 :大家好我是小鱼干儿♛是一个热爱编程、热爱算法的大三学生,蓝桥杯国赛二等奖获得者 🐟 个人主页 :https://blog.csdn.net/qq_52007481 ⭐ 个人社区 :【小鱼干爱编程】 🔥 算法专栏 :算法竞赛进阶指南 💯 刷题网站 :市面上的刷题网站有很多如何选择一个适

    2024年02月03日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包