怎么理解AST,并实现手写babel插件

1,268 阅读8分钟

程序表面看就是文本文件里的字符,计算机首先对其进行词法、语法分析,然后用某种计算机能理解的低级语言来重新表达程序,而这其中AST就是一个重点。

水平有限,粗浅的说下AST的理解,并通过手写babel插件来加深理解。

先看看, JS 是怎么编译执行的

  • 词法分析,将原始代码生成Tokens
  • 语法分析,将Tokens生成抽象语法树(Abstract Syntax Tree,AST)
  • 预编译,当 JavaScript 引擎解析脚本时,它会在预编译期对所有声明的变量和函数进行处理!并且是先预声明变量,再预定义函数!
  • 解释执行,在执行过程中,JavaScript 引擎是严格按着作用域机制(scope)来执行的,并且 JavaScript 的变量和普通函数作用域是在定义时决定的,而不是执行时决定的。

ast_3 后两者暂不介绍,先简单理解下词法分析和语法分析:

词法分析

词法分析:将原始代码转化成最小单元的词语数组,最小单元的词语数组的专业名词是Tokens,这里注意,词语会加上相应的类型。

比如:var a = 'hello'

词法分析之后输出Tokens如下:

[
  {
    type: "Keyword",
    value: "var",
  },
  {
    type: "Identifier",
    value: "a",
  },
  {
    type: "Punctuator",
    value: "=",
  },
  {
    type: "String",
    value: "'hello'",
  },
];

可借助网站esprima在线生成。

语法分析

语法分析:将Tokens按照语法规则,输出抽象语法树,抽象语法树其实就是JSON对象

将 JS 进行生成抽象语法树的网站:astexplorer

比如:var a = 'hello'

语法分析之后生成抽象语法树如下:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": "hello",
            "raw": "'hello'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

借助网站esprimaastexplorer生成。

也可以借助esprima库生成:

const esprima = require("esprima");
const createAst = (code) => esprima.parseModule(code, { tokens: true });
const code = `var a="hello"`;
const ast = createAst(code);
console.log(ast);

注意到这里,是代码生成AST然后编译,但也可以用AST转化成代码

看着好像AST跟日常代码有点远,但其实一直在接触:

  • webpacklint 核心都是通过 AST 对代码进行检查、分析
  • UglifyJS 通过 AST 实现代码压缩混淆
  • babel核心通过AST而实现代码转换功能

以下要一步步实践~

修改 AST 生成新的代码

AST 怎么转成 代码

借助esprima将代码转化成AST,同理可以可借助escodegen将 AST 转化为代码.

const esprima = require("esprima");
// code变成AST的函数
const createAst = (code) => esprima.parseModule(code, { tokens: true });

const escodegen = require("escodegen");
// AST变成code的函数
let astToCode = (ast) => escodegen.generate(ast);

const code = `var a="hello"`;
const ast = createAst(code);

const newCode = astToCode(ast);
// var a = 'hello';
console.log(newCode);

怎么修改 AST

AST 其实就是 JSON 对象,当然怎么修改对象,就怎么修改 AST 啦。

比如想将var a='hello' 变成 const a='hello',先看下两个代码的 AST,然后将前者修改成和后者一样就好啦!

const_var

细看看只有,kind 那里不一样,这样就简单啦!

// ...同上面
const code = `var a="hello"`;
const ast = createAst(code);

// 直接修改kind
ast.body[0].kind = "const";

const newCode = astToCode(ast);
// const a = 'hello';
console.log(newCode);

但简单的这样直接修改很容易,一旦代码变得复杂,嵌套层次变多,或者修改的代码变多,上面的方式就难过了,这里借助另外一个工具库estraverse,遍历找到需要的地方,然后修改

怎么遍历 AST

AST 虽然是一个 JSON 对象,但是可以以树的结构去理解,而estraverse是以深度优先的方式遍历 AST 的。

凡是带所有属性 type 的都是一个节点。

DFS_1

也可以用代码直观的理解,estraverse怎么遍历AST的:

const esprima = require("esprima");
// code变成AST的函数
const createAst = (code) => esprima.parseModule(code, { tokens: true });

const code = `var a="hello"`;
const ast = createAst(code);

// 遍历
const estraverse = require("estraverse");
let depth = 0;
// 层次越深,缩进就越多
const createIndent = (depth) => " ".repeat(depth);

estraverse.traverse(ast, {
  enter(node) {
    console.log(`${createIndent(depth)} ${node.type} 进入`);
    depth++;
  },
  leave(node) {
    depth--;
    console.log(`${createIndent(depth)} ${node.type} 离开`);
  },
});

对照着生成的 AST 看,很明显就是深度遍历的过程:

 Program 进入
  VariableDeclaration 进入
   VariableDeclarator 进入
    Identifier 进入
    Identifier 离开
    Literal 进入
    Literal 离开
   VariableDeclarator 离开
  VariableDeclaration 离开
 Program 离开

借助 estraverse 修改 AST

比如:var a='hello'; var b='world'var修改成const的话

// ..createAst astToCode函数同上面
const estraverse = require("estraverse");

const code = `var a='hello'; var b='world'`;
const ast = createAst(code);

estraverse.traverse(ast, {
  enter(node) {
    // 凡是var的都改成const
    if (node.kind === "var") {
      node.kind = "const";
    }
  },
});

const newCode = astToCode(ast);
// const a = 'hello'; const b = 'world';
console.log(newCode);

通过estraverse,很方便的,将旧的 AST增删改其中的结点,从而生成想要的新的 AST!

用 babel-types 快速生成一个 AST

上面的生成一个 AST 总是先有 code 才行,怎么能直接生成 AST 呢?

babel-types!!!!

AST 是由节点构成的,只要生成相应描述的节点就可以哒。

借助babel-types可以生成任意的节点!

比如const a='hello',b='world':

{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "a"
      },
      "init": {
        "type": "Literal",
        "value": "hello",
        "raw": "'hello'"
      }
    },
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "b"
      },
      "init": {
        "type": "Literal",
        "value": "world",
        "raw": "'world'"
      }
    }
  ],
  "kind": "const"
}

其实就是 init + id => VariableDeclarator + kind => VariableDeclaration

const kind = "const";
const id = t.identifier("a");
const init = t.stringLiteral("hello");

const id2 = t.identifier("b");
const init2 = t.stringLiteral("world");

// a=hello
const variableDeclarator = t.variableDeclarator(id, init);
const variableDeclarator2 = t.variableDeclarator(id2, init2);

const declarations = [variableDeclarator, variableDeclarator2];
const ast = t.variableDeclaration(kind, declarations);

// 这里的ast就是上面展开的
console.log(ast);

type 名一般就是相应的 API 名,同级的其他属性就是 API 的参数。

怎么写一个 babel 插件

babel 最重要的功能就是将ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法。

因为转化的地方非常多,babel一般以插件的形式,通过babel-core连接插件,从而转换代码。

假设将const转化为var的话:

const t = require("babel-types");

// 插件
const ConstPlugin = {
  visitor: {
    // path是对应的type的路径,其属性node也可以理解为一个描述const表达式的对象
    // 遍历到VariableDeclaration的时候,就把kind换成var
    VariableDeclaration(path) {
      let node = path.node;
      console.log(node);
      if (node.kind === "const") {
        node.kind = "var";
      }
    },
  },
};

// 试下写的插件
const code = `const a='hello'`;

const babel = require("babel-core");
let newCode = babel.transform(code, { plugins: [ConstPlugin] });
// var a = 'hello';
console.log(newCode.code);

手写实现插件 babel-plugin-arrow-functions

babel-plugin-arrow-functions的功能:将箭头函数转化为普通函数

var a = (s) => {
  return s;
};
var b = (s) => s;

// 转化成
var a = function (s) {
  return s;
};
var b = function (s) {
  return s;
};

先看下箭头函数和转化成普通函数之后的 AST 区别:

ast_2

  • 注意,body 的类型如果不是BlockStatement的话,就换成BlockStatement;是的话,不用管
  • type 是ArrowFunctionExpression的节点,可以换成FunctionExpression节点

因为写babel插件,所以必须借助babel-core转换成新代码:

// npm i babel-core babel-types
const t = require("babel-types");

// 插件
const ArrowPlugin = {
  visitor: {
    ArrowFunctionExpression(path) {
      let node = path.node;
      let { params, body } = node;
      // 如果不是代码块的话,生成一个代码块
      if (!t.isBlockStatement(body)) {
        // 生成return语句
        let returnStatement = t.returnStatement(body);
        // 创建代码块语句
        body = t.blockStatement([returnStatement]);
      }
      // 利用t生成一个等价的普通函数的表达式对象
      const functionJSON = t.functionExpression( null, params, body, false, false );
      // 替换掉当前path
      path.replaceWith(functionJSON);
    },
  },
};

// 试下写的插件
const arrowJsCode = `var a = (s) => { return s; }; var b = (s) => s;`;

const babel = require("babel-core");
let functionJSCode = babel.transform(arrowJsCode, { plugins: [ArrowPlugin] });
// var a = function (s) { return s; };var b = function (s) { return s; };
console.log(functionJSCode.code);

引用