抽象语法树初探

1,767 阅读6分钟

抽象语法树

什么是抽象语法树?

抽象语法树英文表示为(Abstract Syntax Tree),或者一般简称为(Syntax tree),是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

抽象语法树有什么用?

代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等

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

代码混淆压缩

  • UglifyJS2 等

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

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

JavaScript Parser

那如何将我们平时写的代码变换成抽象语法树呢?我们可以借助一些常用的工具:

  • esprima
  • traceur
  • acorn
  • shift

他们都可以称之为 JavaScript Parser 是能够把JavaScript源码转化为抽象语法树的解析器。

一个例子

我们有这样一个函数:

function ast(){}

经过抽象语法树解析、改变内容、重新生成源码的流程后改造后,我们想要获得:

function newAst(){}

这样说起来可能还是有些抽象,我们借助一些在线工具astexplorer可以简单的查看下ast的结构:

01.png

从图中我们可以看到,抽象语法树其实是一个对象,type 字段标识的是层级结构类型,里面还有一些字段,标识具体的类型、参数等等。

我们要做的就是将函数生成这样的结构,然后解析,最后返回出一个新的函数。

我们首先安装依赖:

npm i esprima estraverse escodegen -S
  • esprima: 这个包能将源代码生成抽象语法树
  • estraverse: 这个包能遍历抽象语法树,修改树上的代码
  • escodegen: 将抽象语法树,重新生成代码
let esprima = require("esprima") // 把JS源代码转成AST语法树
let estraverse = require("estraverse") // 遍历语法树, 修改树上的节点
let escodegen = require("escodegen") // 把AST语法树重新转换成代码

// 源代码 是一个函数声明,函数名字为 ast
let code = `function ast(){}`
// 第一步:将源代码生成抽象语法树
let ast = esprima.parse(code)
// 缩进样式
let indent = 0
const padding = () => " ".repeat(indent)

// 调用 traverse方法遍历语法树
estraverse.traverse(ast, {
  enter(node) {
    console.log(padding() + node.type + "进入")
    // 如果是函数声明
    if (node.type === "FunctionDeclaration") {
      // 将函数的名称修改为  newAst
      node.id.name = "newAst"
    }
    indent += 2
  },
  leave(node) {
    indent -= 2
    console.log(padding() + node.type + "离开")
  },
})

// 用修改过的ast 生成新的代码
const result =  escodegen.generate(ast)

console.log(result)

通过打印,结合上面那张图的,我们可以看到代码处理的流程如下。

Program进入
  FunctionDeclaration进入
    Identifier进入
    Identifier离开
    BlockStatement进入
    BlockStatement离开
  FunctionDeclaration离开
Program离开

function newAst() {
}

至此,我们已经借助工具完成了一次简单的语法树的处理分析。

通过上面的小例子,是不是感觉似曾相识,如果你有这种感觉就对了,这个就特别类似于我们在平时工作中使用的babel。

什么是babel?

简单来说,babel 能够转译ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行。 就像上面的小例子一样,babel的工作过程也可以分为三个部分:

  • Parse(解析) 将源代码转换成抽象语法树,树上有很多的节点
  • Transform(转换) 对抽象语法树进行转换
  • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

我们可以用一张图展示这三个步骤:

ast-compiler-flow.jpeg

介绍几个babel的常用包

我们在日常开发中,但凡观察下package.json中的依赖,就肯定有这个这几个包:

  • @babel/core: Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能。需要注意的是,这个包本身并不知道你想转换哪些内容,比如你是否想转换箭头函数,你是否想转换装饰器?这些能力需要安装特定的插件配合这个包使用。
  • @babel/traverse: 用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点.
  • @babel/generate: 可以把AST生成源码,同时生成sourcemap
  • @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用。

使用babel利用ast的原理转换ES6代码

既然已经知道了 babel的工作原理,我们不妨使用babel提供的工具来写一个小例子,加深理解,就拿 箭头函数为例,我们想利用babel 将箭头函数转换为普通函数。

安装依赖

npm install @babel/core babel-types babel-plugin-transform-es2015-arrow-functions -D

小提示:在书写本篇文章时,安装的依赖版本如下:

"@babel/core": "^7.16.5",
"babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
"babel-types": "^6.26.0"

上面的安装命令中的babel-plugin-transform-es2015-arrow-functions就是babel的一个插件,这个插件的作用就是当源代码中匹配到箭头函数的时候,使用这个插件来转换,可以这么理解,babel/core 中提供了插件的机制,可以在解析到箭头函数的时候,调用这箭头函数插件来处理这种特定的语法格式。

事实上,插件就是一个钩子函数,在遍历语法树的过程中,可以捕获某些特定类型的节点并进行转换,每一个ES6的语法都会对应这样一个插件,每个插件都会捕获自己的语法节点,转换对应的ES6的语法。

我们平时在开发的过程中为了方便,会将所有的插件打成一个包,@babel/preset-env 这其实是一个插件集合。

// 这是babel的核心包
let babelCore = require("@babel/core")
// 箭头函数插件
let arrowFunctionsPlugin = require("babel-plugin-transform-es2015-arrow-functions") 
// 源代码是一个箭头函数
let sourceCode = `
  const sum = (a,b) => {
    console.log(this)
    return a + b
  }
`;

// 调用 babel的转化能力
let targetCode = babelCore.transform(sourceCode,{
  // 使用的是箭头函数转换插件
  plugins:[arrowFunctionsPlugin]
})

console.log(targetCode.code)

查看控制台,可以看到打印出来转换后的代码:

var _this = this;

const sum = function (a, b) {
  console.log(_this);
  return a + b;
};

至此,我们已经使用原生的babel工具完成了箭头函数的转换。