JS AST 与 Babel

2025-03-15

了解 AST

AST,全称为抽象语法树(Abstract syntax tree)

环境的安装 npm install @babel/core

在编译原理中,从源码到机器码的过程,中间还需要进过很多步骤。

比如,源码通过词法分析器变为记号序列,再通过语法分析器变为AST,再通过语义分析器,一步步往下编译,最后变成机器码

可以简单理解为将 js 代码,按照规则,解析成一份json数据,通过此json,按照规则,可以还原出 js 代码。此时可以修改 json 中的结构,来解析出新的 js 代码

JS 中语法结构大体分为三大类

  • Expression 表达式 - 存在值的语句
  • Statement 语句 / 陈述式 - 执行一件事
  • Declaration 声明

**Tips: ** 逻辑符号 &&! 等,接收的必须是表达式。python 不同,js 可以使用 true && console.log(1)!(a=1)

  • js 中赋值是表达式,python 中赋值是语句
  • python 中 print(1) 虽然是调用表达式,但 python2 中 print 是语句,虽然 python3 中print变更为函数,但仍然存在限制。因此 print 函数调用不能用于逻辑表达式内
    def log(msg):
        print(msg)
    
    True and log(1)  # ✅ 该语句可正常运行
    True and print(1)  # ❌ 异常
    

了解AST结构

通过解析网站 https://astexplorer.net/open in new window,选择语言 javascript ,解析引擎 @babel/parser。了解 ast 结构

下面是一些说明,路径中大写表示该节点的type值,本文会用 type 值来简称这些节点。根节点为 File 节点。参考文章https://juejin.cn/post/7051838561967931429open in new window

  • 代码内容数组(每个语句都是body中的一项) File -> program -> body

    // 移除部分信息的结构树
    {
      "type": "File",
      "program": {
        "type": "Program",
        "body": []
      }
    }
    
  • 变量声明

    • 变量声明语句(数组中 type 为 VariableDeclaration 的节点) File -> program -> body[ VariableDeclaration ]

    • 变量声明类型 File -> program -> body[ VariableDeclaration ] -> kind ,常见取值如 varletconst

    • 声明变量数组(声明多个变量 var a,b 的情况存在,因此为数组形式) File -> program -> body[ VariableDeclaration ] -> declarations

    • 声明变量节点(数组中 type 为 VariableDeclarator 的节点) File -> program -> body[ VariableDeclaration ] -> declarations[ VariableDeclarator ]

      // 移除部分信息的结构树
      {
        "type": "File",
        "program": {
          "body": [
            {
              "type": "VariableDeclaration",
              "declarations": [],
              "kind": "let"
              ...
      
    • 变量标识符为标识符节点(标识符节点的 type 都为 Identifier,不论变量还是函数) File -> program -> body[ VariableDeclaration ] -> declarations[ VariableDeclarator ] -> id

    • 变量标识符名 File -> program -> body[ VariableDeclaration ] -> declarations[ VariableDeclarator ] -> id -> name 即 Identifier.name

      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "b"
              ...
      
    • 声明变量初始化赋值节点 声明变量节点 VariableDeclaration -> declarations[ VariableDeclarator ] 存在属性 init。init的值为一个字面量节点(直接写死的值),仅在变量声明时做了初始化才存在 init 属性

      • 数字字面量:init的值为 NumericLiteral 节点,value 为赋值内容

        // b = 1
        // 省略部分内容
        {
          "type": "VariableDeclarator",
          },
          "id": {
            "type": "Identifier",
            "name": "b"
          },
          "init": {
            "type": "NumericLiteral",
            "value": 1
          }
        }
        
      • 字符串字面量:init的值为 StringLiteral 节点,value 为赋值内容

        // let b = 'b'
        // 省略部分内容
        {
          "type": "VariableDeclaration",
          "declarations": [
            {
              "type": "VariableDeclarator",
              "id": {
                "type": "Identifier",
                "name": "b"
              },
              "init": {
                "type": "StringLiteral",
                "value": "b"
              }
            }
          ],
          "kind": "let"
        },
        
      • 箭头函数: init的值为 ArrowFunctionExpression 节点, body 的值为 BlockStatement 节点(该节点在函数中详细说明)。

        {
          "type": "VariableDeclaration",
          "declarations": [
            {
              "type": "VariableDeclarator",
              "id": {
                "type": "Identifier",
                "name": "b"
              },
              "init": {
                "type": "ArrowFunctionExpression",
                "id": null,
                "generator": false,
                "async": false,
                "params": [],
                "body": {
                  "type": "BlockStatement",
                  "body": [],
                }
              }
            }
          ],
          "kind": "let"
        },
        
      • 数组字面量:ArrayExpression 节点,elements 属性是一个赋值节点数组,用于存放初始化内容。

        • a = [],elements 为空数组

          // let b = []
          {
            "type": "VariableDeclaration",
            "declarations": [
              {
                "type": "VariableDeclarator",
                "id": {
                  "type": "Identifier",
                  "name": "b"
                },
                "init": {
                  "type": "ArrayExpression",
                  },
                  "elements": []
                }
              }
            ],
            "kind": "let"
          },
          
        • a=[1, 'a', function () {}, null, true] elements 为 [ NumericLiteral 节点,StringLiteral 节点,FunctionExpression 节点,BooleanLiteral 节点, NullLiteral 节点 ]

  • 特殊的变量:对象

    {
      "type": "VariableDeclaration",  // 声明行
      "declarations": [
        {
          "type": "VariableDeclarator",  // 声明项
          "id": {  // 声明项标识符
            "type": "Identifier",
            "name": "a"
          },
          "init": {  // 初始化
            "type": "ObjectExpression",  // 对象表达式 {key : value}
            "properties": [
              {
                "type": "ObjectProperty",  // 对象属性
                // ...
              },
              // ...
            ]
          }
        }
      ],
      "kind": "let"
    },
    
    • 对象声明初始化的属性 init 声明变量节点 VariableDeclaration -> declarations[ VariableDeclarator ] 存在属性 init,init节点的 type 为 ObjectExpression

    • 初始化的属性设置数组。ObjectExpression 节点存在属性 properties数组(对象赋值时有多属性赋值情况,因此为数组形式) File -> program -> body[ VariableDeclaration ] -> declarations[ VariableDeclarator ] -> id -> init -> prop

    • 属性设置节点,即 properties 中的 type 为 ObjectProperty。

      • 对象的属性为数字,如 a = {1:'a'}。ObjectProperty.key 为数字字面量 NumericLiteral 节点,ObjectProperty.value 为字符串字面量节点

        {
          "type": "ObjectProperty",
          "method": false,
          "key": {
            "type": "NumericLiteral",
            "value": 1
          },
          "computed": false,
          "shorthand": false,
          "value": {
            "type": "StringLiteral",
            "value": "a"
          }
        }
        
      • 对象属性为字符串,如 a = {b: 1}。ObjectProperty.key 为标识符 Identifier 节点,ObjectProperty.key.name 为属性标识符 'b'

        {
          "type": "ObjectProperty",
          "method": false,
          "key": {
            "type": "Identifier",
            "name": "b"
          },
          "computed": false,
          "shorthand": false,
          "value": {
            "type": "NumericLiteral",
            "value": 1
          }
        }
        
      • 对象属性值为函数 a = {c: funcion () {}} ObjectProperty.value 为 FunctionExpression 节点

        {
          "type": "ObjectProperty",
          "method": false,
          "key": {
            "type": "Identifier",
            "name": "c"
          },
          "computed": false,
          "shorthand": false,
          "value": {
            "type": "FunctionExpression",  // 函数节点,该节点属性见后续
            "id": null,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "body": [],
              "directives": []
            }
          }
        }
        
  • 表达式

    // var e = 1+2 初始化时的表达式
    // VariableDeclarator.init 为 BinaryExpression 节点
    {
      "type": "BinaryExpression",
      "left": {
        "type": "NumericLiteral",
        "value": 1
      },
      "operator": "+",
      "right": {
        "type": "NumericLiteral",
        "value": 2
      }
    }
    // var e; e = 1+2;  独立的表达式
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "AssignmentExpression",
        "operator": "=",
        "left": {
          "type": "Identifier",
          "name": "e"
        },
        "right": {
          "type": "BinaryExpression",
          "left": {
            "type": "NumericLiteral",
            "value": 1
          },
          "operator": "+",
          "right": {
            "type": "NumericLiteral",
            "value": 2
          }
        }
      }
    },
    
    • 逻辑运算 LogicalExpression :a && b, x || y

    • 三元表达式 ConditionalExpression :a ? b : c

    • 二元运算 BinaryExpression a + b, x * y

    • 一元运算 UnaryExpression:!x, typeof y, -z

    • 自增/自减 UpdateExpression:i++, --j

    • 变量赋值(非变量声明初始化) AssignmentExpression :a = 1, b += 2

    • 逗号表达式 SequenceExpression,SequenceExpression.expressions 数组用于存放逗号切分的多个内容

    • 调用 CallExpression :foo(1, 2)

    • 实例化:NewExpression new Person()

    • 箭头函数:ArrowFunctionExpression :() => 42

    • 函数表达式:FunctionExpression :function() { return 1; }

    • 成员访问:MemberExpression :obj.prop, arr[0]

    • 模板字符串:TemplateLiteral:hello ${name}

  • 函数分为函数表达式 FunctionExpression (var a = function (){}) 与函数定义 FunctionDeclaration (function a (){})。以下为该类节点内的重要内容

    • 函数名节点 FunctionDeclaration.id 为 标识符节点 Identifier,函数名 FunctionDeclaration.id.name,即 Identifier.name。函数表达式的 FunctionExpression.id 为 null
    • 函数参数数组 FunctionExpression.params,内部存放 标识符节点 Identifier。函数无传入参数时,FunctionExpression.params 为 []
    • 函数体节点 FunctionExpression.body 为代码块节点 BlockStatement,FunctionExpression.body.body(BlockStatement.body)为函数内容数组,长度与函数体内容有关
      • 返回值节点:ReturnStatement 节点,argument为返回内容,可以为字面量节点或表达式节点等
    // function c(a,b){return 1}
    // 移除部分信息的结构树
    {
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "c"
      },
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "name": "a"
        },
        {
          "type": "Identifier",
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ReturnStatement",
            "argument": {
              "type": "NumericLiteral",
              "extra": {
                "rawValue": 1,
                "raw": "1"
              },
              "value": 1
            }
          }
        ],
        "directives": []
      }
    }
    

babel api 简介

参考文章https://juejin.cn/post/7051843106898968589open in new window

常用代码结构

常用的编写流程如下

// 1. 导包 需要环境@babel/core
const fs = require('fs');
const parser = require('@babel/parser');  // 将源码解析成 ast
const traverse = require('@babel/traverse').default;  // 遍历ast
const t = require('@babel/types');  // 判断节点类型,构建新节点
const generator = require('@babel/generator').default;  // 将ast解析成js

// 2. 读取目标文件,解析为 ast
const jscode = fs.readFileSync('./target.js', 'utf-8');
const ast = parser.parse(jscode);

// 3. 处理目标js文件
// ...

// 4. 获取js,存储为新文件
const {code} = generator(ast);
fs.writeFileSync('./output.js', code, 'utf-8');

parser

用于将 js 代码转换为AST

官方文档 https://babeljs.io/docs/babel-parseropen in new window

// 此处及后续节点处理操作,会修改 AST(抽象语法树),但它并没有改变变量的指向
const ast = parser.parse(jscode, {
  // 常用配置项
  sourceType: "module"  // 当代码中存在 import export 时,需要配置此选项
})

generator

将AST转换为js对象,<js对象>.code 为转换后的代码

可通过在处理代码时,将一部分节点还原为code并打印,便于调试。具体信息参考 Path 中生成代码的部分

官方文档 https://babeljs.io/docs/babel-generatoropen in new window

const code = generator(ast, {
  // 常用配置项
  retainLine: false,  // 是否使用源代码行号,控制格式化内容
  comments: false,  // 保留注释
  // 压缩代码,行内压缩,不影响行号,配合 retainLine: false 可将代码压缩为单行
  compact: true  // 中压缩,压缩代码,省略不必要的空格,保证行末分号
  // minified: true  // 高压缩 省略不必要的行末分号和空格
  // concise: true // 低压缩 保证空格为1,保留行末分号
  
  // 高级配置项
  jsescOption: {
  	wrap: boolean,
  	minimal: true  // ascii码与unicode自动还原
	}
}).code;

traverse

遍历所给 ast 中的节点

traverse(ast, visitor)
// 参数 ast:目标 ast 节点
// 参数 visitor:过滤方式
  • ast 目标节点。通常为整个加密代码的根节点

  • visitor

    • 声明 visitor

      const visitor = {};
      // 筛选条件。遍历所给节点下的所有函数表达式
      visitor.FunctionExpress = function(path) { // 注意:此处回调函数会接收到满足筛选条件的路径,不是节点
        console.log('find function')
      };
      traverse(ast, visitor);
      
      // 等价代码
      const visitor = {
        // 属性为筛选条件,内容为回调函数
        FunctionExpress: function(path) {
          console.log('find function')
        }
      }
      
      // 等价代码。ES6语法,在对象属性为回调函数时的简写
      const visitor = {
        FunctionExpress(path) {
          console.log('find function')
        }
      }
      
      // 节点访问时机
      const visitor = {
        FunctionExpress: {
          // 进入时或退出时 enter / exit
          enter(path) {  // 同上es6语法,enter属性为回调函数。
            console.log('find function')
          }
        }
      }
      
    • 当节点嵌套时的访问时机

      • enter:先处理该节点,再遍历该节点的子节点。处理可能会对子节点产生影响
      • exit:先遍历该节点的子节点,在遍历到最内层后,依次向上层返回,此时依次判断并处理当前将返回上层的节点
    • 筛选条件为多类型,多类型节点使用同一函数处理。visitor 属性使用

      const visitor = {
        'VariableDeclarator|FunctionDeclaration'(path) {}
      };
      
    • 同一类型节点使用多函数处理。代码来源open in new window

      function log_a(path) { console.log('This is [a] function -- ' + path.node.init.value); }
      function log_b(path) { console.log('This is [b] function -- ' + path.node.init.value); }
      function log_c(path) { console.log('This is [c] function -- ' + path.node.init.value); }
      const visitor =
      {
          'VariableDeclarator': {
              'enter': [log_a, log_c, log_b]
          }
      }
      
  • 遍历部分节点

    const ast = parser.parse(jscode);
    
    // 更新函数参数名
    const updateParamNameVisitor = {
      Identifier(path) {  // 遍历函数中的所有标识符,形参和调用都需修改
        if (path.node.name === this.paramName) {  // 通过this接收额外参数
          path.node.name = 'x';
        }
      }
    }
    
    const visitor = {
      FunctionExpression(path) {
        const paramName = path.node.params[0].name;
        path.traverse(updateParamNameVisitor, {paramName})  // 遍历部分节点
        // 遍历路径.traverse(visitor, {额外参数})
        // 此处是创建对象的简写,完整为 { paramName: paramName }
      }
    }
    
    traverse(ast, visitor)  // 遍历全部节点
    

types

节点类型判断 与 构造新节点

  • 判断节点类型。判断是否为标识符 types.isIdentifier(path.node)。等价代码path.node.type === 'isIdentifier'

    path.node.type === 'isIdentifier' && path.node.name === 'n'
    // 等价
    types.isIdentifier(path.node, {name: 'n'})  // 额外筛选条件通过对象传入
    
  • 生成新节点

    const t = require('@babel/types');
    const generator = require('@babel/generator').default;
    
    
    const right = t.binaryExpression('*', t.numericLiteral(3), t.numericLiteral(4))  // right = 3 * 4
    const binExp = t.binaryExpression('+', t.numericLiteral(2), right)  //  binExp = 2 + right
    const dclr = t.variableDeclarator(t.identifier('x'), binExp)  // 一个变量声明项 - 指明标识符名称与初始化内容 x = binExp
    const ast = t.variableDeclaration('const', [dclr])  // 声明语句 const x = binExp
    
    const code = generator(ast).code;
    console.log(code)
    // const x = 2 + 3 * 4;
    
    // 通过将目标代码放入 ast explorer,可得出大概结构
    // 编写节点时可查看源码确认所需参数
    
    // 例如 t.numericLiteral(3) 等 数字,字符串,布尔,对象,正则...等 静态值
    // 还可以使用 t.valueToNode 生成节点
    const node = t.valueToNode([1, '2', false, null, undefined, /\w/s/g, {x:'1000', y: 2000}])  // 一个包含多个静态常量的静态数组
    const code1 = generator(node).code
    console.log(code1)
    
    // 此处的坑
    // @babel/generator >= 7.24.0
    // 当前Node 环境不支持 RegExp 的多 flag 输出格式(尤其是 /\w/s/g)
    // @babel/generator@7.22.x	输出 /\w/sg	✅ 正常
    // @babel/generator@7.24.x	输出 /\w/s/g	❌ 报错
    // @babel/generator@7.25.7+	输出 /\w/sg	✅ 修复
    

Path

在 visitor 遍历时,回调函数得到的是 Path(NodePath)对象

在解析时,得到的是节点树。当遍历时,会将对应节点对象封装为Path(对当前节点对象进行包装,添加额外的信息属性),传入回调函数

  • 常用属性

    • path.node 当前节点(Node)对象(ast中解析出的json结构)。visitor 中只使用 Node 时,回调函数可编写解包,如: Identifier( {node} ) { console.log(node) }
    • path.parent 父级节点(Node)对象
    • path.parentPath 父级Path(NodePath)对象
    • path.container 同级节点列表
    • path.scope 作用域Scope
  • 常用方法

    • path.stop() 停止遍历在当前 path 及其下的子节点。常见使用方式:visitor中只处理某特征的节点,但不对其子节点处理,完成后调用 stop 停止子节点遍历 **注意,只想处理某一个节点,增加特征判断,使用return跳出。stop只停止子节点遍历,依然会操作后续节点 **

    • path.skip() 遍历当前 path 下的子节点跳过。常见使用方式:visitor在遍历某一类节点时,不处理某些特征的节点,使用 if 判断并调用skip跳过

    • path.get 获取子节点的NodeNodePath

      • 获取子节点 Node. 参考 ast解析结构,对不同种类的节点,通过 Node 访问其子节点对应的属性

        // BinaryExpression 节点
        path.node.left  // 左操作项节点Node
        path.node.right  // 右操作项节点Node
        path.node.operator  // 操作符节点Node
        
      • 获取子节点 NodePath。通过 path.get() 方法获取 NodePath 对象

        // BinaryExpression 节点
        path.get('left')  // 左操作项Path
        path.get('right')  // 右操作项节点Path
        path.get('operator')  // 操作符节点Path
        
        path.get('left.name')  // 多级访问
        
    • 判断类型

      // Node 对象通过 types 判断节点类型 - 判断的是 path.node.type
      types.isIdentifier(path.node)
      types.isIdentifier(path.node, {name: 'n'})  // 额外筛选条件,是不是变量名为n的变量
      
      // NodePath可直接判断 - 判断的是 path.type
      path.isIdentifier()
      path.isIdentifier({name: 'n'})  // 额外筛选条件,是不是变量名为n的变量
      
      // 一般情况下 path.type 与 path.node.type 一致
      
      // 类型断言,不符合抛出异常
      path.assertIdentifier()
      
    • 节点转代码

      // Node: 向 `generator` 传入 `Node` 用于生成代码(**不是Path**)
      generator(path.node).code
      
      // Path: babel通过将 toString 重写,实现 path.toString 将 NodePath 转换成代码
      path.toString()
      path + ''  // 隐式转换。字符串拼接时,会隐式调用 toString
      
    • 替换节点属性

      // BinaryExpression 节点,将二项式的左右属性(两个子节点)分别替换为 x 与 y
      path.node.left = t.identifier('x');
      path.node.right = t.identifier('y');
      
    • 替换当前节点。通过 visitor 遍历需要替换的节点,进行替换。通常需要 path.stop(); 防止死循环

      • path.replaceWith 将一个节点替换为一个新节点

        // 将当前节点替换为数字字面量1
        path.replaceWith(t.valueToNode(1))
        
      • path.replaceWithMultiple 将 path 所在节点替换为多个节点

        // 将单条  return 替换为 两个表达式 与一个 return
        ReturnStatement(path) {
          path.replaceWithMultiple([
            t.expressionStatement(),  // 表达式1
            t.expressionStatement(),  // 表达式2
            t.returnStatement(),
          ]);
        }
        
      • path.replaceInline 将 path 所在节点替换为一个或多个新节点。源码中通过传入类型是否为数组自动调用 replaceWithreplaceWithMultiple

        // 替换成一个新节点 - 等效为 replaceWith
        path.replaceInline(t.valueToNode(1))
        
        // 替换成多个新节点 - 等效为 replaceWithMultiple
        ReturnStatement(path) {
          path.replaceInline([
            t.expressionStatement(),  // 表达式1
            t.expressionStatement(),  // 表达式2
            t.returnStatement(),
          ])
        }
        
      • path.replaceWithSourceString 将 path 所在节点替换为字符串源码对应的节点

        // 返回值的函数混淆 - 将返回值替换为一个函数,真实返回值放入函数中返回
        ReturnStatement(path) {
          const argumentPath = path.get('argument');  // 获取返回值的 argument 节点Path。如果返回值为二项式,则此时是二项式的 Path
          argumentPath.replaceWithSourceString(
            'function() {return ' + argumentPath + '}()'  // 隐式转换。argumentPath 自动 toString 转换成源码字符串
          );
          path.stop();  // 由于生成了一个带返回值函数,不停止会遍历到新生成函数的返回值,再次进行替换,从而进入递归,出现死循环
        }
        
        // 注意替换内容与遍历内容是否一致,需调用 stop ,防止死循环
        
    • path.remove 删除节点

      // 删除多余的分号
      EmptyStatement(path) {
        path.remove();
      }
      
    • 插入节点 path.insertBeforepath.insertAfter,在指定 path 前 与 后插入兄弟节点

  • 父级Path

    • path.parent 父节点Node对象

    • path.parentPath 父节点Path(NodePath)对象,因此path.parentPath.node === path.parent

    • 常用方法

      • path.findParent 对层层父节点 path 进行 find 操作。传入回调函数,回调形参为父节点path,函数体为 true 时返回该 path

        // 获取包含 return 的对象表达式 path
        ReturnStatement(path) {
          // 形参 p 自动读取父节点 path
          // 层层向上,直到 path 所在节点为对象表达式时返回
          console.log(path.findParent((p) => p.isObjectExpression()))
        }
        
        
      • path.find 对当前节点和层层父节点 path 进行 find 操作。与 path.findParent 类似,只多包含了当前节点

      • path.getFunctionParent 获取父函数。find 条件为函数表达式

      • path.getStatementParent 获取语句。find 条件为 Statement

      • path.parentPath.replaceWith(Node) 与Path用法一致,替换父节点

      • path.parentPath.remove 与Path一致,删除父节点

  • 同级Path。介绍 Path 中的 containerlistKeykey 属性。以下方代码中的 return 所在节点为例

    // 例1:目标 return 语句
    const x = function () {return 1};
    
    // 解析结构如下解析
    // FunctionExpression.body = BlockStatement 
    // BlockStatement.body = [ReturnStatement]  
    
    // 例2:目标 属性 b 的 ObjectProperty
    const x = {a:1, b:2, c:3}
    // ObjectExpression.properties = [ObjectProperty, ObjectProperty, ObjectProperty]
    // ObjectProperty.key = Identifier
    // Identifier.name = 'a'
    
    // 例3 例1代码的 BlockStatement
    
    • path.container(容器) : 本节点所在容器的值(可用于获取同级节点)

      • 例1中,ReturnStatement 语句位于 BlockStatement.body 数组中, container 值为 BlockStatement.body 的值(解析的值为节点数组,不是Path),即 [ReturnStatement]
      • 例2中,container 值为 ObjectExpression.properties 的值,即 [ObjectProperty, ObjectProperty, ObjectProperty]
      • 例3中,FunctionExpression.body 不是数组,为 BlockStatement 本身,一个函数只含有一个代码块。container 值为 FunctionExpression 节点,而不是 FunctionExpression.body容器不指向自身)。
    • path.key:从 container 中获取该节点的 container[] 的 key 值

      • 例1中,由于获取该节点使用 container[0],因此 key 值为 0
      • 例2中,由于获取该节点使用 container[1],因此 key 值为 1
      • 例3中,由于 containerFunctionExpression,获取该节点使用 container['body'],因此 key 值为 'body'
    • path.listKey:容器对应的属性名

      • 例1中,containerBlockStatement.body,因此 listKey'body'
      • 例2中,containerObjectExpression.properties,因此 listKey'properties'
      • 例3中,container 不是数组,因此为 undefined
    • path.inList :判断 container 是否为数组

    • container 为数组时的常用方法(可用于操作同级节点)

      • path.getSibling:使用索引获取从 container 中同级节点 path.getSibling(index)

        // 获取当前节点的下一个节点
        path.getSibling(path.key + 1)  // path.key 为当前节点的索引,`+1` 指向下一个节点
        
      • path.parentPath.unshiftContainercontainer 前部插入多个节点

        // 将给定的节点数组放入当前节点所在容器的最前方(保持传入数组的顺序)
        // 指明容器位置: 父节点   的          body     节点数组
        path.parentPath.unshiftContainer('body', [t.valueToNode(1), t.valueToNode(2)]);
        // 返回值为新节点的Path数组(传入的是节点,返回的是包装为Path)
        
        
        // 通过returnStatement修改函数参数  function(a, b) {return 1}
        // AST结构:	FunctionExpression.params -> [Idenetifier, Idenetifier]
        // 					FunctionExpression.body -> BlockStatement
        // 					BlockStatement.body -> [ReturnStatement]
        // 指定容器位置  FunctionExpression.params ,新增两个参数 x 和 y
        path.parentPath.parentPath.unshiftContainer('params', [t.Idenetifier('x'), t.Idenetifier('y')])
        
      • path.parentPath.pushContainercontainer 结尾追加节点

      // 指明容器位置: 父节点   的      body    节点
      path.parentPath.pushContainer('body', t.valueToNode(3))
      // 返回新节点的Path数组(数组固定只有一个成员,传入的是节点,返回的是包装为Path)
      

scope

使用 Path 对象的 scope 属性获取与处理指定Path的作用域相关内容

  • path.scope.block 获取标识符的作用域,得到作用域 Node 对象

    var a = 1
    const b = 2
    var func = function() {
      let c = 3
      function func2() {}
    }
    
    • 变量作用域。例子,获取变量 c 的作用域,assert:FuncionExpression

      // 变量c的作用域为函数 func
      Identifier(path) {
        if (path.node.name == 'c') {
          console.log(path.scope.block)  // FuncionExpression
        }
      }
      
    • 函数作用域。例子,获取函数 func2 的作用域,assert:FuncionExpression

      // func 的函数声明是函数表达式(匿名函数赋值)
      // func2 为函数声明
      FunctionDclaration(path) {
        path.scope.block  // 与变量不一样,直接获取到的作用域是函数自己
        path.scope.parent.block. // 父级作用域才是该函数真实的作用域
      }
      
  • Binding 对象

    • path.scope.getBinding 获取当前节点上绑定的指定的标识符(当前节点可用的标识符,标识符作用域包含当前的 path)。返回值为 Binding 对象(对当前标识符节点对象进行包装,添加额外的信息属性)

      // ***** 案例1 *****
      var a = 1
      const b = 2
      var func = function() {
        let c = 3
        function func2() {}
      }
      
      // func2 path
      FunctionDclaration(path) {
        path.scope.getBinding(a)  // func2 可以读取变量 a ,因此可获得全局变量 a 的 Binding
      }
      
      // ***** 案例2 *****
      var a = 1
      function f(a) {
        a = 10
        return a
      }
      // f 函数 path
      FunctionDeclaration(path) {
        path.scope.getBinding(a)  // a 变量重名,优先获取函数形参 a 的 Binding
      }
      
      // ***** Binding对象 *****
      {
        identifier: 标识符 Node 对象,
        scope: Scope {
          block: 作用域 Node 对象,
          path: 作用域 Node 的 NodePath 对象
        },
        path: 标识符 NodePath 对象,  // 其中 含有 container:[[Node]]、listKey:params 等容器信息
        kind: 'param',  // 该标识符类型
        constant: true,  // 是否为常量
        constantViolations: [...],  // 变量被修改的 NodePath 数组(声明时的赋值不计算在内) 
                             // !!重点,根据使用情况,对该变量操作,不影响其他节点
                             // 仅依靠变量名称判断无法避免全局变量与局部变量重名时的误改
        
        // 引用相关
        referencePaths: [...],  // 变量被引用的 NodePath 数组  
          // !!重点,根据使用情况,对该变量操作,不影响其他节点
          // 仅依靠变量名称判断无法避免全局变量与局部变量重名时的误改
        referenced: true, // 是否引用(在 return 中引用)
        references:1,  // 引用次数, 本例中,函数内的 `a=10` 只属于赋值,不属于引用
        ...
      }
      
    • scope.hasBinding 方法,判断是否有绑定。false 等效为 getBinding 方法返回 undefined

    • 通过 Binding 对象获取作用域。除了通过 Identifier 过滤变量名,得到 path 后获取 scope 外,还可以通过该标识符的 Binding 对象获取作用域(避免之前 案例2 中的变量 a 重名情况)

      // ***** 案例 *****
      var a = 1
      const b = 2
      var func = function() {  // FunctionExpression
        let c = 3
        function func2() {}  // FunctionDeclaration
      }
      
      // 案例中的变量 c
      FunctionExpression(path) {  // 定位变量所在函数
        const bindingC = path.scope.getBinding('c');  // 获取 c 的 Binding 对象
        bindingC.scope.block  // 通过 Binding 对象获取作用域
      }
      
    • path.scope.getOwnBinding 用于获取当前节点自己的绑定。如父节点中定义的变量等内容,不会通过此函数获取(如案例1中,函数内无法通过 getOwnBinding 获取全局变量)。由于存在问题,使用时需要注意

      // getOwnBinding 案例
      let a
      const func1 = function() {
        a = 1;
        let b = 2;
        function func2() {
          let c = 3;
        }
        func2();
      }
      
      // 遍历 func1 内的标识符,查看哪些是 OwnBinding
      FunctionExpression(path) {
        path.traverse({
          Identifier(p) {
            const name = p.node.name;
            // 强转为 boolean,是否为 OwnBinding
            console.log(name, !!p.scope.getOwnBinding(name))
          }
        })
      }
      
      // 输出
      a false  // a 是全局变量
      b true  // b 是该函数内定义的
      func2 false  // **重点** 即使为函数内的定义,此时仍然不是 Own , 需要注意避免
      c true  // **重点** 子函数内的也视为 Own,但理论上不应该为 Own , 需要注意避免。需要额外判断作用域范围是否为当前标识符的作用域
      func2 true  // **重点** 函数内的定义的子函数,引用时才是 Own
      
      
      // 避免 getOwnBinding 的问题,增加作用域一致性判断
      FunctionExpression(path) {
        path.traverse({
          Identifier(p) {
            const name = p.node.name;
            const binding = p.scode.getBinding(name);
            // binding 与 作用域 所在代码是否一致
            binding && console.log(name, generator(binding.scope.block).code == path + '');
          }
        });
      }
      
    • scope.hasOwnBinding 方法,同理

    • scope.getAllBinding 方法,获取当前节点的所有绑定。返回值类型为 Object,key 为 标识符名,value 为 Binding 对象。一样有 getOwnBinding 注意的点

  • 遍历作用域,通过 scope.traverse 遍历作用域中的节点

    // 遍历作用域 案例
    let a
    const func1 = function() {
      a = 1;
      let b = 2;
      function func2() {
        let c = 3;
      }
      func2();
    }
    
    // 遍历 b 作用域中的节点
    FunctionExpression(path) {  // 定位函数 func2
      const binding = path.scope.getBinding('b')  // 获取 b 的 Binding 对象 - func1的函数体 - b的作用范围,包括声明
      binding.scope.traverse(binding.scope.block, {  // 传入作用域 Node 对象 - func1的函数体
        AssignmentExpression(p) {  // 赋值表达式
          if (p.node.left.name == 'b') {  // 定位 b 在哪里进行
            p.node.right = t.t.stringLiteral('bbb')  // 修改 b 的值
          }
        }
      })
    }
    
  • scope.rename 方法可用于标识符重命名,并修改引用处

    • 重命名指定变量

      const func1 = function() {
        let a = 1;
        function func2() {
          a = 2;
        }
        func2();
      }
      
      // 通过变量的 Binding 修改
      FunctionExpression(path) {
      	const binding = path.scope.getBinding('a');
        binding.scope.rename('a', 'b');  // binding.scope 会在作用域内生效
      }
      
      // 使用 path.scope.rename 需要保证遍历到全部节点,遍历条件需为 Identifier
      
    • scope.generateUidIdentifier 方法生成标识符名。避免手动指定时,指定了一个已存在的标识符名

      // 首次调用
      path.scope.generateUidIdentifier('_id')
      // 返回值:{type: 'Identifier', name: '_id'}
      
      // 第二次调用
      path.scope.generateUidIdentifier('_id')
      // 返回值:{type: 'Identifier', name: '_id2'}
      
    • 简单的标识符混淆

      // 将所有标识符从 _0x28ba 起,按顺序重命名
      Idetifier(path) {
        path.scope.rename(path.node.name, path.scope.generateUidIdentifier('_0x28ba'), name)
      }
      
      /*
      多次获取时的效果
      {type: 'Identifier', name: '_0x28ba'}
      {type: 'Identifier', name: '_0x28ba2'}
      {type: 'Identifier', name: '_0x28ba3'}
      {type: 'Identifier', name: '_0x28ba4'}
      {type: 'Identifier', name: '_0x28ba5'}
      {type: 'Identifier', name: '_0x28ba6'}
      {type: 'Identifier', name: '_0x28ba7'}
      {type: 'Identifier', name: '_0x28ba8'}
      {type: 'Identifier', name: '_0x28ba9'}
      {type: 'Identifier', name: '_0x28ba0'}
      {type: 'Identifier', name: '_0x28ba1'}
      {type: 'Identifier', name: '_0x28ba10'}
      {type: 'Identifier', name: '_0x28ba11'}
      {type: 'Identifier', name: '_0x28ba12'}
      */
      
    • scope.hasReference 方法。hasReference('a') 查看当前节点是否有标识符 a 的引用,返回值类型为 boolean。等效为标识符 a 的 Binding 对象中 referenced 的值

    • scope.getBindingIdentifier 方法。getBindingIdentifier('a') 获取当前节点绑定的标识符 a 的 Node 对象(获取 Identifier 节点)

    • scope.getOwnBindingIdentifier 方法。与 scope.getBindingIdentifier 同理