JS AST 与 Babel
了解 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/,选择语言 javascript ,解析引擎 @babel/parser。了解 ast 结构
下面是一些说明,路径中大写表示该节点的type值,本文会用 type 值来简称这些节点。根节点为 File 节点。参考文章https://juejin.cn/post/7051838561967931429
代码内容数组(每个语句都是body中的一项) File -> program -> body
// 移除部分信息的结构树 { "type": "File", "program": { "type": "Program", "body": [] } }变量声明
变量声明语句(数组中 type 为 VariableDeclaration 的节点) File -> program -> body[ VariableDeclaration ]
变量声明类型 File -> program -> body[ VariableDeclaration ] -> kind ,常见取值如
var,let,const声明变量数组(声明多个变量
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": [] } }- 函数名节点 FunctionDeclaration.id 为 标识符节点 Identifier,函数名 FunctionDeclaration.id.name,即 Identifier.name。函数表达式的 FunctionExpression.id 为
babel api 简介
参考文章https://juejin.cn/post/7051843106898968589
常用代码结构
常用的编写流程如下
// 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-parser
// 此处及后续节点处理操作,会修改 AST(抽象语法树),但它并没有改变变量的指向
const ast = parser.parse(jscode, {
// 常用配置项
sourceType: "module" // 当代码中存在 import export 时,需要配置此选项
})
generator
将AST转换为js对象,
<js对象>.code为转换后的代码
可通过在处理代码时,将一部分节点还原为code并打印,便于调试。具体信息参考 Path 中生成代码的部分
官方文档 https://babeljs.io/docs/babel-generator
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) {} };同一类型节点使用多函数处理。代码来源
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获取子节点的Node和NodePath获取子节点
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 所在节点替换为一个或多个新节点。源码中通过传入类型是否为数组自动调用replaceWith或replaceWithMultiple// 替换成一个新节点 - 等效为 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.insertBefore与path.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 条件为 Statementpath.parentPath.replaceWith(Node)与Path用法一致,替换父节点path.parentPath.remove与Path一致,删除父节点
同级Path。介绍 Path 中的
container,listKey,key属性。以下方代码中的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代码的 BlockStatementpath.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(容器不指向自身)。
- 例1中,
path.key:从container中获取该节点的container[]的 key 值- 例1中,由于获取该节点使用
container[0],因此key值为0 - 例2中,由于获取该节点使用
container[1],因此key值为1 - 例3中,由于
container为FunctionExpression,获取该节点使用container['body'],因此key值为'body'
- 例1中,由于获取该节点使用
path.listKey:容器对应的属性名- 例1中,
container为BlockStatement.body,因此listKey为'body' - 例2中,
container为ObjectExpression.properties,因此listKey为'properties' - 例3中,
container不是数组,因此为undefined
- 例1中,
path.inList:判断container是否为数组container为数组时的常用方法(可用于操作同级节点)path.getSibling:使用索引获取从container中同级节点path.getSibling(index)// 获取当前节点的下一个节点 path.getSibling(path.key + 1) // path.key 为当前节点的索引,`+1` 指向下一个节点path.parentPath.unshiftContainer:container前部插入多个节点// 将给定的节点数组放入当前节点所在容器的最前方(保持传入数组的顺序) // 指明容器位置: 父节点 的 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.pushContainer:container结尾追加节点
// 指明容器位置: 父节点 的 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 需要保证遍历到全部节点,遍历条件需为 Identifierscope.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同理