AST抽象语法树

2019-10-2 AST

# AST抽象语法树

# 前言

每种语言都有很多解析器,使用方式和生成的结果各不相同,开发者可以根据需要选择合适的解析器。(总结就是前端的任何一种语言都可以转化成可以描述的json)

常见的语言转换器: JavaScript

  • 最知名的当属babylon,因为他是babel的御用解析器,一般JavaScript的AST这个库比较常用
  • acron:babylon就是从这个库fork来的

HTML

  • htmlparser2:比较常用
  • parse5:不太好用,还需要配合jsdom这个类库

CSS

  • cssom、csstree等
  • less/sass

XML

  • Xmldom

虚拟dom

  • snabbdom

注意:个人观点,我们常说的虚拟dom其实也是一种解析器,主要是解决js与html的依赖关系。

# 常见的语法树转换工具

babel-parser,recast,esprima

Babel为当前最流行的代码JavaScript编译器了,其使用的JavaScript解析器为babel-parser (opens new window),最初是从Acorn 项目fork出来的。Acorn 非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。esprima的实现也是基于Acorn的。  recast 是基于 @babel/core@babel/parser 、@babel/types等包进行封装开发的。

总结:基本上都来源于 Acorn ,接下来我们主要分析baber-paser。 **

# Babel 运行阶段

三个重要的步骤。

# 解析

首先需要将 JavaScript 字符串经过词法分析、语法分析后,转换为计算机更易处理的表现形式,称之为“抽象语法树(AST)”,这个步骤我们使用了 Babylon (opens new window) 解析器。

# 转换

当 JavaScript 从字符串转换为 AST 后,我们就能更方便地对其进行浏览、分析和有规律的修改,根据我们的需求,将其转换为新的 AST,babel-traverse (opens new window) 是一个很好的转换工具,使得我们能够很便利的操作 AST 。

# 生成

最后,我们将修改完的 AST 进行反向处理,生成 JavaScript 字符串,整个转换过程也就完成了,这一步当中,我们使用到了 babel-generator (opens new window) 模块。

# 什么是AST

之前听过一句话:“如果你能熟练地操作 AST ,那么你真的可以为所欲为。”,当时并不理解其含义,直到真正了解 AST 后,才发现 AST 对编程语言的重要性是不可估量的。 在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

JavaScript 程序一般是由一系列字符组成的,我们可以使用匹配的字符([], {}, ()),成对的字符('', "")和缩进让程序解析起来更加简单,但是对计算机来说,这些字符在内存中仅仅是个数值,并不能处理这些高级问题,所以我们需要找到一种方式,将其转换成计算机能理解的结构。 我们简单看下面的代码:

let a = 2;

将其转换为 AST 会是怎样的呢,我们使用 astexplorer (opens new window) 在线 AST 转换工具,可以得到以下树结构: image.png json 结构如下: image.png

为了更形象表述,我们将其转换为更直观的结构图形:

image.png

AST 的根节点都是 Program ,这个例子中包含了两部分:

  1. 一个变量申明(VariableDeclarator),将标识符(Identifier) a 赋值为数值(NumericLiteral) 3。

  2. 一个二元表达式语句(BinaryExpression),描述为标志符(Identifier)为 a,操作符(operator) + 和数值(NumericLiteral) 5。

这只是一个简单的例子,在实际开发中,AST 将会是一个巨型节点树,将字符串形式的源代码转换成树状的结构,计算机便能更方便地处理,我们使用的 Babel 插件,也就是对 AST 进行插入/移动/替换/删除节点,创建成新的 AST ,再将 AST 转换为字符串源代码,这便是 Babel 插件的原理,之所以能够“为所欲为”,其原因就是可以将原始代码按照指定逻辑转换为你想要的代码。

很多时候我们不明白这个代表什么含义:VariableDeclarator,Identifier,这个有篇文章JavaScript抽象语法树AST (opens new window),都解释了单词对应含义。(或者babel的官方文档 (opens new window))

例如

# VariableDeclarator

变量声明,kind 属性表示是什么类型的声明,因为 ES6 引入了 const/let。

interface VariableDeclaration <: Declaration {
    type: "VariableDeclaration";
    declarations: [ VariableDeclarator ];
    kind: "var" | "let" | "const";
}

# Identifier

标识符,就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符。相应的接口是这样的:

interface Identifier {
    type: 'Identifier';
    name: string;
}

# 怎么使用?

简单案例

let a=2;
[1,2].push((val)=>{console.log(val)})
var {parse} = require('@babel/parser');
var t = require('@babel/types');
var traverse = require('@babel/traverse').default
 
var generate = require('@babel/generator').default;

var code = "let a=2;[1,2].push((val)=>{console.log(val)})";
const ast = parse(code);
traverse(ast, {
    CallExpression(path, state) {
        if (path.get('callee').isMemberExpression()) {
          if (path.get('callee').get('object').isIdentifier()) {
            if (path.get('callee').get('object').get('name').node == 'console') path.remove()
          }
        }
      // path.remove()
    },
  VariableDeclaration:function (path) {
    path.node.kind ='var';``
  }
})
const output = generate(ast, {
  /* options */ }, code);
  console.log(output);

{ code: 'var a = 2;\n[1, 2].push(val => {});',
  map: null,
  rawMappings: null }

总结:菜鸟千万不要尝试使用抽象语法改变代码,否者可能你的代码讲不是你的代码。

有兴趣可以看这个项目 (opens new window)

通过开发 Babel 插件来理解什么是抽象语法树(AST) (opens new window)

高级前端基础-JavaScript抽象语法树AST (opens new window)

**

最后更新: 2019-10-2 10:01:15 ├F10: AM┤