JS `AST` (抽象语法树) 遍历与操作:`esprima`, `estraverse`, `escodegen`

欢迎来到JavaScript AST魔法学院!

大家好,我是你们今天的讲师,魔法师梅林(化名,怕你们记住真名)。今天我们要学习的是JavaScript AST(抽象语法树)的强大魔法,并学会如何使用 esprima, estraverse, 和 escodegen 这三件神器来驾驭它。

别害怕,AST听起来很高大上,其实就像解剖一棵语法树,然后想怎么摆弄它就怎么摆弄它!准备好了吗?让我们开始吧!

什么是AST?(语法树的秘密)

想象一下,你的JavaScript代码是一棵树,每个节点代表代码的一部分,比如变量、函数、运算符等等。AST就是这棵树的抽象表示。它把代码分解成计算机更容易理解的结构。

举个例子,代码 const x = 1 + 2; 的AST可能长这样(简化版):

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "x"
          },
          "init": {
            "type": "BinaryExpression",
            "operator": "+",
            "left": {
              "type": "Literal",
              "value": 1,
              "raw": "1"
            },
            "right": {
              "type": "Literal",
              "value": 2,
              "raw": "2"
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

是不是有点眼花缭乱?别担心,我们不用手写AST,有工具帮我们生成!

三大神器:esprima, estraverse, escodegen

这三位是AST世界的黄金搭档:

  • esprima: 解析器,负责把JavaScript代码变成AST。就像一个翻译官,把人类语言翻译成机器语言。
  • estraverse: 遍历器,负责在AST这棵树上行走,访问每个节点。就像一个导游,带你参观AST的每一个角落。
  • escodegen: 代码生成器,负责把AST重新变成JavaScript代码。就像一个作家,把机器语言还原成人类语言。

1. esprima:代码的解剖刀

esprima 负责把JavaScript代码解析成AST。用法非常简单:

const esprima = require('esprima');

const code = 'const x = 1 + 2;';
const ast = esprima.parseScript(code);

console.log(JSON.stringify(ast, null, 2)); // 打印AST结构

这段代码会把 const x = 1 + 2; 解析成我们前面看到的AST结构。 JSON.stringify(ast, null, 2) 是为了方便我们阅读,把JSON格式化一下。

esprima 还可以配置一些选项,比如:

选项 描述
loc 是否包含每个节点的源代码位置信息(行号和列号)。 true 或者 false, 默认false
range 是否包含每个节点的源代码范围信息(起始和结束索引)。 true 或者 false, 默认false
tokens 是否返回 token 数组, token 是代码的最小词法单元,比如关键词、标识符、运算符等等。true 或者 false, 默认false
comment 是否返回注释数组。 true 或者 false, 默认false
sourceType 指定代码类型,可以是 "script" (默认) 或 "module" (用于 ES 模块)。
tolerant 是否容忍语法错误。 如果设置为 true,解析器会尝试忽略错误并继续解析。 true 或者 false, 默认false
jsx 是否支持 JSX 语法。 true 或者 false, 默认false

例如:

const ast = esprima.parseScript(code, { loc: true, range: true, tokens: true });

2. estraverse:AST的探险家

有了AST,我们就要想办法访问它。 estraverse 就是用来遍历AST的。它提供了两种遍历方式:

  • estraverse.traverse(ast, { enter: ..., leave: ... }): 这是最常用的方法,它会深度优先遍历AST。
    • enter 函数在进入一个节点时被调用。
    • leave 函数在离开一个节点时被调用。
const estraverse = require('estraverse');

const code = 'const x = 1 + 2; console.log(x);';
const ast = esprima.parseScript(code);

estraverse.traverse(ast, {
  enter: function(node, parent) {
    // 在进入每个节点时执行
    console.log('Entering node:', node.type);
  },
  leave: function(node, parent) {
    // 在离开每个节点时执行
    console.log('Leaving node:', node.type);
  }
});

这段代码会在遍历AST的过程中,打印出每个节点的类型。

  • 控制遍历流程: enterleave 函数可以返回一个特殊的值来控制遍历流程:
    • estraverse.VisitorOption.Skip:跳过当前节点的所有子节点。
    • estraverse.VisitorOption.Break:停止遍历。
    • estraverse.VisitorOption.Remove:移除当前节点(需要配合 estraverse.replace 使用)。

例如,跳过 BinaryExpression 节点的子节点:

estraverse.traverse(ast, {
  enter: function(node, parent) {
    if (node.type === 'BinaryExpression') {
      console.log('Skipping BinaryExpression');
      return estraverse.VisitorOption.Skip;
    }
  }
});

3. escodegen:代码的炼金术士

有了AST,也访问过了,现在我们要把它变回代码。 escodegen 就是用来把AST生成JavaScript代码的。

const escodegen = require('escodegen');

const code = 'const x = 1 + 2;';
const ast = esprima.parseScript(code);

const generatedCode = escodegen.generate(ast);
console.log(generatedCode); // 输出:const x = 1 + 2;

escodegen 也提供了一些选项来控制代码生成的方式,比如:

选项 描述
format 控制代码格式化的选项,包括缩进、换行符等等。可以设置 indentnewlinespace 等属性。
comment 是否保留AST中的注释。 默认为 true
sourceMap 是否生成 source map。 如果设置为 true, 会返回一个包含 generated code 和 source map 的对象。
sourceCode 原始源代码,用于生成 source map。
verbatim 是否保留原始代码中的字面量, 例如字符串和数字的引号和格式。

例如,生成格式化后的代码:

const generatedCode = escodegen.generate(ast, {
  format: {
    indent: {
      style: '  ',
      base: 0
    },
    newline: 'n',
    space: ' '
  }
});

实战演练:给代码加点料!

现在,让我们用这三件神器来做一些有趣的事情。

1. 自动给函数加上console.log

这个例子演示了如何遍历AST,找到所有的函数声明,并在函数体的开头加上 console.log 语句。

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

const code = `
function add(a, b) {
  return a + b;
}

const multiply = function(a, b) {
  return a * b;
};

class MyClass {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log('Hello, ' + this.name);
  }
}
`;

const ast = esprima.parseScript(code);

estraverse.traverse(ast, {
  enter: function(node, parent) {
    if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'MethodDefinition') {
      // 创建一个 console.log 语句的 AST
      const consoleLogNode = {
        "type": "ExpressionStatement",
        "expression": {
          "type": "CallExpression",
          "callee": {
            "type": "MemberExpression",
            "object": {
              "type": "Identifier",
              "name": "console"
            },
            "property": {
              "type": "Identifier",
              "name": "log"
            },
            "computed": false,
            "optional": false
          },
          "arguments": [
            {
              "type": "Literal",
              "value": `Function ${node.id ? node.id.name : 'anonymous'} called`,
              "raw": `"${`Function ${node.id ? node.id.name : 'anonymous'} called`}"`
            }
          ],
          "optional": false
        }
      };

      // 将 console.log 语句添加到函数体的开头
      if (node.type === 'MethodDefinition') {
        // MethodDefinition 的 body 是一个 FunctionExpression
        node.value.body.body.unshift(consoleLogNode);
      } else {
        node.body.body.unshift(consoleLogNode);
      }
    }
  }
});

const generatedCode = escodegen.generate(ast);
console.log(generatedCode);

这段代码会给所有函数(包括函数声明、函数表达式和类方法)的开头加上 console.log 语句,方便我们调试。

2. 替换变量名

这个例子演示了如何替换代码中的变量名。

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

const code = 'const oldVarName = 10; console.log(oldVarName);';
const ast = esprima.parseScript(code);

estraverse.traverse(ast, {
  enter: function(node, parent) {
    if (node.type === 'Identifier' && node.name === 'oldVarName') {
      node.name = 'newVarName'; // 替换变量名
    }
  }
});

const generatedCode = escodegen.generate(ast);
console.log(generatedCode); // 输出:const newVarName = 10; console.log(newVarName);

这段代码会把所有名为 oldVarName 的变量替换成 newVarName

3. 移除debugger语句

这个例子演示了如何移除代码中的 debugger 语句。

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

const code = 'debugger; const x = 1; debugger; console.log(x); debugger;';
const ast = esprima.parseScript(code);

estraverse.traverse(ast, {
  enter: function(node, parent) {
    if (node.type === 'DebuggerStatement') {
      return estraverse.VisitorOption.Remove; // 移除 debugger 语句
    }
  }
});

const generatedCode = escodegen.generate(ast);
console.log(generatedCode); // 输出:const x = 1; console.log(x);

这段代码会移除代码中所有的 debugger 语句。

高级技巧:

  • AST Explorer: 强烈推荐使用 AST Explorer 这个工具,它可以实时显示代码的AST结构,方便你理解AST的各个节点。

  • 代码风格工具: 像 ESLint 和 Prettier 这样的代码风格工具,底层也是基于AST来实现的。它们通过分析AST,检查代码是否符合规范,并自动格式化代码。

  • 代码转换工具: 像 Babel 这样的代码转换工具,也是基于AST来实现的。它可以把ES6+的代码转换成ES5的代码,让你的代码可以在旧版本的浏览器上运行。

总结

今天我们学习了JavaScript AST的基本概念和用法,以及如何使用 esprima, estraverse, 和 escodegen 这三件神器来操作AST。

AST是一个强大的工具,它可以让你深入理解JavaScript代码的结构,并对代码进行各种各样的操作。掌握AST,你就可以编写出更智能、更高效的工具,提高你的开发效率。

希望今天的课程对你有所帮助。记住,魔法是需要练习的,多动手实践,你才能真正掌握AST的强大力量!下次再见,各位魔法师们!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注