手把手带你入门 AST 抽象语法树

AST 是什么

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

AST 有什么用

AST 运用普遍,好比:java

  • 编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
  • elintpretiier 对代码错误或风格的检查;
  • webpack 经过 babel 转译 javascript 语法;

而且若是你想了解 js 编译执行的原理,那么你就得了解 AST。node

AST 如何生成

js 执行的第一步是读取 js 文件中的字符流,而后经过词法分析生成 token,以后再经过语法分析( Parser )生成 AST,最后生成机器码执行。webpack

整个解析过程主要分为如下两个步骤:git

  • 分词:将整个代码字符串分割成最小语法单元数组
  • 语法分析:在分词基础上创建分析语法单元之间的关系

JS Parser 是 js 语法解析器,它能够将 js 源码转成 AST,常见的 Parser 有 esprima、traceur、acorn、shift 等。github

词法分析

词法分析,也称之为扫描(scanner),简单来讲就是调用 next() 方法,一个一个字母的来读取字符,而后与定义好的 JavaScript 关键字符作比较,生成对应的Token。Token 是一个不可分割的最小单元:web

例如 var 这三个字符,它只能做为一个总体,语义上不能再被分解,所以它是一个 Token。npm

词法分析器里,每一个关键字是一个 Token ,每一个标识符是一个 Token,每一个操做符是一个 Token,每一个标点符号也都是一个 Token。除此以外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。编程

最终,整个代码将被分割进一个tokens列表(或者说一维数组)。json

语法分析

语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法若是有错的话,抛出语法错误。

说了这么多咱们来看下 javaScript 代码片断转成 AST 以后是什么样的咱们拿一行简单的代码来展现

🌰例子 1

const fn = a => a;
复制代码

如图从这个 AST 语法树咱们就可以很清楚的看出一个代码他的具体含义,而且使用的是什么语法,方法等。

用人话翻译这个图就是:用类型 const 声明变量 fn 指向一个箭头函数表达式,它的参数是 a 函数体也是 a。

🌰例子 2

const fn = a => {
    let i = 1;
  return a + i;
};
复制代码

咱们来看 body 这块:

🌰例子 3

函数调用

function test(){
  let a = 1;
  console.log(a)
}
复制代码

主要看 MemberExpression

以上截图均是使用 Acorn 解析。使用 Acorn 的缘由是据我了解在 parser 解析中,Acorn 是公认的最快的。而且咱们使用的 Webpack 打包工具中 babel 用的也是 Acorn。

上述截图的属性是 AST 的一部分,这个结构包含了不少属性。

  • VariableDeclaration 变量声明
  • VariableDeclarator 变量声明的描述
  • Expression 表达式节点

更多属性展现:

  1. 能够去 AST explorer 能够在线看到不一样的 parser 解析 js 代码后获得的 AST。
  2. github 上看全部的 ESTree ESTree
  3. 关于属性介绍的文档 抽象语法树AST介绍

实战 AST 的运用

题目

经过上面介绍的 console.log AST,下面咱们就来完成一个在调用 console.log(xx) 时候给前面加一个函数名,这样用户在打印时候能改方便看到是哪一个函数调用的。

举例

// 源代码
function getData() {
  console.log("data")
}

// --------------------

// 转化后代码
function getData() {
  console.log("getData", "data");
}
复制代码

介绍

首先介绍下咱们须要使用的工具 Babel

  • @babel/parser : 将 js 代码 ------->>> AST 抽象语法树;
  • @babel/traverseAST 节点进行递归遍历;
  • @babel/types 对具体的 AST 节点进行进行修改;
  • @babel/generator : AST 抽象语法树 ------->>> 新的 js 代码;

为何使用 babel ? 主要是比较好用(只对这个比较熟悉😭)。

进入 @babel/parser 官网开头就介绍了它是使用的 Acorn 来解析 js 代码成 AST 语法树(说明确实 Acorn 比较好)。

开始码起来

  1. 新建文件打开控制台安装须要的包
cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D
复制代码
  1. 建立 js 文件, 编写大体布局以下 使用 AST
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function compile(code) {
  // 1.parse 将代码解析为抽象语法树(AST)
  const ast = parser.parse(code);

  // 2,traverse 转换代码
  traverse.default(ast, {});

  // 3. generator 将 AST 转回成代码
  return generator.default(ast, {}, code);
}

const code = ` function getData() { console.log("data") } `;
const newCode = compile(code)
复制代码

使用 node 跑出结果,由于什么都没处理,输出的是原代码,

完善 compile 方法

function compile(code) {
  // 1.parse
  const ast = parser.parse(code);

  // 2,traverse
  const visitor = {
    CallExpression(path) {
      // 拿到 callee 数据
      const { callee } = path.node;
      // 判断是不是调用了 console.log 方法
      // 1. 判断是不是成员表达式节点,上面截图有详细介绍
      // 2. 判断是不是 console 对象
      // 3. 判断对象的属性是不是 log
      const isConsoleLog =
        types.isMemberExpression(callee) &&
        callee.object.name === "console" &&
        callee.property.name === "log";
      if (isConsoleLog) {
        // 若是是 console.log 的调用 找到上一个父节点是函数
        const funcPath = path.findParent(p => {
          return p.isFunctionDeclaration();
        });
        // 取函数的名称
        const funcName = funcPath.node.id.name;
        // 将名称经过 types 来放到函数的参数前面去
        path.node.arguments.unshift(types.stringLiteral(funcName));
      }
    }
  };
  // traverse 转换代码
  traverse.default(ast, visitor);

  // 3. generator 将 AST 转回成代码
  return generator.default(ast, {}, code);
}
复制代码

纯代码看起来比较难理解下面是我将上面的 path.node 写入到文件中给你们看下数据格式。

{
  "type": "CallExpression",
  "start": 24,
  "end": 43,
  "loc": {
    "start": { "line": 3, "column": 2 },
    "end": { "line": 3, "column": 21 }
  },
  "callee": {
    "type": "MemberExpression",
    "start": 24,
    "end": 35,
    "loc": {
      "start": { "line": 3, "column": 2 },
      "end": { "line": 3, "column": 13 }
    },
    "object": {
      "type": "Identifier",
      "start": 24,
      "end": 31,
      "loc": {
        "start": { "line": 3, "column": 2 },
        "end": { "line": 3, "column": 9 },
        "identifierName": "console"
      },
      "name": "console"
    },
    "property": {
      "type": "Identifier",
      "start": 32,
      "end": 35,
      "loc": {
        "start": { "line": 3, "column": 10 },
        "end": { "line": 3, "column": 13 },
        "identifierName": "log"
      },
      "name": "log"
    },
    "computed": false
  },
  "arguments": [
    {
      "type": "StringLiteral",
      "start": 36,
      "end": 42,
      "loc": {
        "start": { "line": 3, "column": 14 },
        "end": { "line": 3, "column": 20 }
      },
      "extra": { "rawValue": "data", "raw": "'data'" },
      "value": "data"
    }
  ]
}

复制代码

咱们将没必要要的位置信息(start, end, loc)属性删除,对照数据来看代码将会一目了然

再跑该文件

很好,调用 console.log 方法参数前面增长了函数名,完成!!

为了你们可以方便运行,下面是完整代码

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");


function compile(code) {
  // 1.parse
  const ast = parser.parse(code);

  // 2,traverse
  const visitor = {
    CallExpression(path) {
      const { callee } = path.node;
      const isConsoleLog =
        types.isMemberExpression(callee) &&
        callee.object.name === "console" &&
        callee.property.name === "log";
      if (isConsoleLog) {
        const funcPath = path.findParent(p => {
          return p.isFunctionDeclaration();
        });
        const funcName = funcPath.node.id.name;
        fs.writeFileSync("./funcPath.json", JSON.stringify(funcPath.node), err => {
          if (err) throw err;
          console.log("写入成功");
        });
        path.node.arguments.unshift(types.stringLiteral(funcName));
      }
    }
  };
  traverse.default(ast, visitor);

  // 3. generator
  return generator.default(ast, {}, code);
}

const code = ` function getData() { console.log('data') } `;
console.log(compile(code).code);

复制代码

看到这里,若是你以为都没什么问题,相信你对 AST 已经有了很清楚的认识了,而且对 babel 编译代码也有了必定的理解,之后写 webpack 配置也就不会对 babel 那么陌生了。

总结

为了兼容低版本浏览器 咱们也一般会使用 webpack 打包编译咱们的代码将 ES6 语法下降版本,好比箭头函数变成普通函数。将 const、let 声明改为 var 等等,他都是经过 AST 来完成的,只不过实现的过程比较复杂,精致。不过也都是这三板斧:

  1. js 语法解析成 AST;
  2. 修改 AST;
  3. AST 转成 js 语法;

最后

有时间,你们在尝试完成以后也一样能够试试箭头函数转普通函数等一些经常使用的代码转换,这样能够很好的加深印象。

全文章,若有错误或不严谨的地方,请务必给予指正,谢谢!

参考

相关文章
相关标签/搜索