AST in JS

Babel,Webpack,vue-cli和esLint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的。在前端当中AST的使用场景非常广,比如在vue.js当中,我们在代码中编写的template转化成render function的过程当中第一步就是解析模版字符串生成AST。JS的许多语法为了给开发者更好的编程体验,并不适合不适合进程的理解。所以需要把源码转化为AST来更适合进程分析,浏览器的编译器一般会把源码转化为AST来进行进一步的分析来进行其他操作。通过了解AST这个概念,对深入了解前端的一些框架和工具是很有帮助的。

本文将从以下几部分进行总结:

  1. AST的使用场景
  2. AST的定义
  3. JavaScript Parser(三板斧)
  4. 利用AST转化箭头函数
  5. 利用AST实现预计算的babel插件

AST的使用场景

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
    • IDE的错误提示、格式化、高亮、自动补全等等
  • 代码混淆压缩
    • UglifyJS2等
  • 优化变更代码,改变代码结构使达到想要的结构
    • 代码打包工具webpack、rollup等等
    • CommonJS、AMD、CMD、UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript
  • AST的定义

    • AST的官方定义:

    在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

    这是在线的AST转换器:AST转换器。代码转化成AST后的格式大致如下图所示:

    AST in JS
    AST in JS

    转化成AST之后的JSON格式大致为:

    {
      "type": "Program",
      "start": 0,
      "end": 16,
      "body": [
        {
          "type": "FunctionDeclaration",
          "start": 0,
          "end": 16,
          "id": {
            "type": "Identifier",
            "start": 9,
            "end": 12,
            "name": "ast"
          },
          "expression": false,
          "generator": false,
          "params": [],
          "body": {
            "type": "BlockStatement",
            "start": 14,
            "end": 16,
            "body": []
          }
        }
      ],
      "sourceType": "module"
    }
    复制代码

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

    JavaScript Parser(三板斧)

    • JavaScript Parser,把js源码转化为抽象语法树的解析器。

    • 浏览器会把js源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。

    • 一般来说每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了详细SpiderMonkey AST format的详细说明,算是业界的标准。

    JS Parser的三板斧

    1.通过 esprima 把源码转化为AST

    let esprima = require('esprima');
    let code = 'function ast(){}';
    let ast = esprima.parse(code);
    console.log(ast);
    复制代码

    通过npm i esprima -S安装之后,运行以上代码,会输出:

    Script {
      type: 'Program',
      body:
       [ FunctionDeclaration {
           type: 'FunctionDeclaration',
           id: [Identifier],
           params: [],
           body: [BlockStatement],
           generator: false,
           expression: false,
           async: false } ],
      sourceType: 'script' }
    复制代码

    2.通过 estraverse 遍历并更新AST

    let esprima = require('esprima');
    let estraverse = require('estraverse');
    let code = 'function ast(){}';
    let ast = esprima.parse(code);
    estraverse.traverse(ast, {
      enter(node) {
        console.log('enter', node.type)
        if (node.type == 'Indentifier') {
          node.name += 'enter';
        }
      },
      leave(node) {
        console.log('leave', node.type)
        if (node.type == 'Indentifier') {
          node.name += 'leave';
        }
      }
    })
    console.log(ast);
    复制代码

    通过npm i estraverse -S安装之后,运行以上代码,会输出:

    Script {
      type: 'Program',
      body:
       [ FunctionDeclaration {
           type: 'FunctionDeclaration',
           id: [Identifier],
           params: [],
           body: [BlockStatement],
           generator: false,
           expression: false,
           async: false } ],
      sourceType: 'script' }
    复制代码

    3.通过 escodegen 将AST重新生成源码

    t esprima = require('esprima');
    let estraverse = require('estraverse');
    let escodegen = require('escodegen');
    let code = 'function ast(){}';
    let ast = esprima.parse(code);
    estraverse.traverse(ast, {
      enter(node) {
        console.log('enter', node.type)
        if (node.type == 'Identifier') {
          node.name += '_enter';
        }
      },
      leave(node) {
        console.log('leave', node.type)
        if (node.type == 'Identifier') {
          node.name += '_leave';
        }
      }
    });
    let result = escodegen.generate(ast)
    console.log(result);
    复制代码

    通过npm i escodegen -S安装完之后,执行以上代码,会输出:

    function ast_enter_leave() {
    }
    复制代码

    这样一来,就把

    function ast() {
    }
    复制代码

    修改为了:

    function ast_enter_leave() {
    }
    复制代码

    转化箭头函数

    利用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实现预计算的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}
      ]
    });
    复制代码