推荐一个可视化的AST工具:https://astexplorer.net

    推荐一篇文章:AST for JavaScript Developers (文章所用解析器Babylon已合并到@babel/parse,非Babylon.js,另一个同名的3D开源库)

    Javascript 代码中的语法单元主要包括以下这么几种:

    • 关键字:例如 varletconst
    • 标识符:没有被引号括起来的连续字符,可能是一个变量,也可能是 ifelse 这些关键字,又或者是 truefalse 这些内置常量
    • 运算符: +-*/
    • 数字:像十六进制,十进制,八进制以及科学表达式等语法
    • 字符串:因为对计算机而言,字符串的内容会参与计算或显示
    • 空格:连续的空格,换行,缩进等
    • 注释:行注释或块注释都是一个不可拆分的最小语法单元
    • 其他:大括号、小括号、分号、冒号等

    写个小功能:箭头函数转成ES5中的普通函数,代码详见Github

    1. const add =(a, b) => a + b;

    先看一眼通过工具解析出来的AST结构:carbon (1).png

    Babel已经提供了丰富的处理AST的方法,一般仅需通过parse解析源代码为AST,然后进行transform转为需要的代码格式。

    // AST遍历器,针对指定类型节点处理
    const visitor = {
      ArrowFunctionExpression(path) {
        console.log(path);
        const params = path.node.params;
        // 偷懒写法,其实应该是遍历节点内容,然后逐条转换
        const blockStatement = types.blockStatement([
          types.returnStatement(
            types.binaryExpression(
              "+",
              types.identifier("a"),
              types.identifier("b")
            )
          ),
        ]);
    
        const func = types.functionExpression(
          null,
          params,
          blockStatement,
          false,
          false
        );
    
        path.replaceWith(func);
      },
    };
    
    const result = babel.transform(arrowCodeString, {
      plugins: [
        {
          visitor,
        },
      ],
    });
    console.log(result.code);//即为普通函数代码
    
    const sum = function() {
      return a + b;
    };
    

    转换后的抽象语法树解构如下图:
    carbon (2).png
    关键在于定义Visitor进行转换,如在Webpack中使用Babel的预置插件,即是使用官方实现的Visitor。

    {
      "plugins": ["@babel/plugin-transform-spread"]
    }
    

    可参考源码实现:babel-plugin-transform-spread/visitor

    {
      //核心代码,返回一个对象包含如下主要对象
      ArrayExpression,    //数组中解构
      CallExpression,        //函数调用时解构
      NewExpression            //对象构造时解构
    }
    

    以ArrayExpression为例:

    ArrayExpression(path) {
            const { node, scope } = path;
            const elements = node.elements;
            if (!hasSpread(elements)) return;
    
            const nodes = build(elements, scope);
            let first = nodes[0];
    
            // If there is only one element in the ArrayExpression and
            // the element was transformed (Array.prototype.slice.call or toConsumableArray)
            // we know that the transformed code already takes care of cloning the array.
            // So we can simply return that element.
            if (nodes.length === 1 && first !== elements[0].argument) {
              path.replaceWith(first);
              return;
            }
    
            // If the first element is a ArrayExpression we can directly call
            // concat on it.
            // `[..].concat(..)`
            // If not then we have to use `[].concat(arr)` and not `arr.concat`
            // because `arr` could be extended/modified (e.g. Immutable) and we do not know exactly
            // what concat would produce.
            if (!t.isArrayExpression(first)) {
              first = t.arrayExpression([]);
            } else {
              nodes.shift();
            }
    
            path.replaceWith(
              t.callExpression(
                t.memberExpression(first, t.identifier("concat")),
                nodes,
              ),
            );
          },