验证码验证
Created At : 2024-04-17 15:11
Views 👀 :
1 学习网站
2 基本组件 2.1 parser
描述:将 js 代码转换为抽象语法树 用法:
1 2 3 let parse = require ("@babel/parser" ).parse let traverse = require ("@babel/traverse" )
2.2 generator
描述:将抽象语法树还原为 JS 代码 用法:
1 2 let generate = require ("@babel/generator" ).default generate (ast).code
2.3 traverse
描述:遍历 node 节点 用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 traverse (ast,{ StringInditify : function ( ){ } }) traverse (ast,{ StringInditify : { exit : function ( ){ } } })
3 基本属性 3.1 Path 属性:
path.replaceWith 把这个节点替换成另外一个节点, 接收参数为 node
path.node 获取当前路径对应的节点。
path.parent 获取当前路径对应节点的父节点。
path.parentPath 获取当前路径对应节点的父路径。
path.scope 表示当前 path 下的作用域,这个也是写插件经常会用到的。
path.container 用于获取当前 path 下的所有兄弟节点(包括自身)。
path.type 获取当前 path 的节点类型。
path.key 获取当前 path 的 key 值,key 通常用于 path.get 函数。
函数:
path.get(key) 获取当前路径下指定属性名(key)对应的子路径。例如,path.get(“body”) 获取当前路径下名为 “body” 的子路径。
path.getSibling(index) 获取当前路径对应节点的兄弟节点的路径。通过指定索引(index)可以获取相应的兄弟路径。
path.getFunctionParent() 获取当前路径对应节点的最近的函数父节点的路径。
path.getPrevSibling() 获取当前 path 的前一个兄弟节点,返回的是 path 类型。
path.getAllPrevSiblings() 获取当前 path 的所有前兄弟节点,返回的是 Array 类型,其元素都是 path 类型。
path.getNextSibling() 获取当前 path 的后一个兄弟节点,返回的是 path 类型。
path.getAllNextSiblings() 获取当前 path 的所有后兄弟节点,返回的是 Array 类型,其元素都是 path 类型。
path.evaluate() 用于计算表达式的值,大家可以参考 constantFold 插件的写法。
path.findParent() 向上查找满足回调函数特征的 path,即判断上级路径是否包含有 XXX 类型的节点。
path.find() 功能与 path.findParent 方法一样,只不过从当前 path 开始进行遍历。
path.getFunctionParent() 获取函数类型父节点,如果不存在,返回 null。
path.getStatementParent() 获取 Statement 类型父节点,这个基本上都会有返回值,如果当前遍历的是 Program 或者 File 节点,则会报错。
path.getAncestry() 获取所有的祖先节点,没有实参,返回的是一个 Array 对象。
path.isAncestor(maybeDescendant) 判断当前遍历的节点是否为实参的祖先节点.
path.isDescendant(maybeAncestor) 判断当前遍历的节点是否为实参的子孙节点.
path.traverse(visitor) 遍历当前路径下的所有子节点,并应用指定的 visitor。
path.replaceWith(node) 用指定的节点替换当前路径对应的节点。
path.remove() 从 AST 中移除当前路径对应的节点。
path.insertBefore(nodes) 在当前路径对应节点之前插入一个或多个节点。
path.insertAfter(nodes) 在当前路径对应节点之后插入一个或多个节点。
path.toString() 用于将 AST 节点转换回对应的源代码字符串。
3.2 Scope 函数:
scope.block 表示当前作用域下的所有 node,参考上面的 this.block = node;
scope.dump() 输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数
scope.crawl() 重构 scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。
scope.rename(oldName, newName, block) 修改当前作用域下的的指定的变量名,oldname、newname 表示替换前后的变量名,为字符串。注意,oldName 需要有 binding,否则无法重命名。
scope.traverse(node, opts, state) 遍历当前作用域下的某些(个)插件。和全局的 traverse 用法一样。
scope.getBinding(name) 获取某个变量的 binding,可以理解为其生命周期。包含引用,修改之类的信息
属性:
scope.block 可以用来获取标识符的作用域,返回 Node
对象,使用方法分为两种情况:变量
和 函数
,变量获取当前作用域,函数获取函数本身作用域。
3.2.1 实例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const a = 1000 ;let b = 2000 ;let obj = { name : 'mankvis' , add : function (a ) { a = 400 ; b = 300 ; let e = 700 ; function demo ( ) { let d = 600 ; } demo (); return a + a + b + 1000 + obj.name ; }, } obj.add (100 );
3.2.2 scope.block 变量获取 block 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const ast = parser.parse (jsCode);traverse (ast, { Identifier (path) { if (path.node .name === 'e' ) { console .log (generator (path.scope .block ).code ) } } })
函数获取 block 属性:
1 2 3 4 5 6 7 8 9 10 11 12 traverse (ast, { FunctionDeclaration (path) { console .log (generator (path.scope .block ).code ) } })
3.2.3 scope.dump 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 traverse (ast, { FunctionDeclaration (path) { console .log ('\n\n这里是函数' , path.node .id .name + '()' ); path.scope .dump (); } })
‘#’开头的是每一个作用域 ,上面有三个作用域,分别是 FunctionDeclaration、FunctionExpression 和 Program。
‘-‘开头的是每一个绑定(binding),每一个 binding 都会包含几个关键信息,分别是:constant、references、violations、kind。
constant 表示是否为常量,为布尔值
references 表示被引用的次数
violations 表示被重新定义的次数
kind 表示声明类型,param 参数、hoisted 提升、var 变量、local 内部。
3.2.4 scope.getBinding 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 traverse (ast, { FunctionDeclaration (path) { let bindingA = path.scope .getBinding ('a' ); console .log (bindingA); }, })
getBinding 中传入的值必须是当前节点能够引用到的标识符名,如果传入一个不存在的 g ,这个标识符并不存在,或者说当前节点引用不到,那么 getBinding 会返回 undefined 由于 FunctionDeclaration 的作用域只是 demo 本身, demo 本身的作用域是 add 这个函数,所以是可以直接找到 a 的
identifier 是 a 标识符的 Node 对象
path 是 a 表示符的 Path 对象
kind 表示这是一个参数,但是并不代表就是当前 demo 的参数。实际上原始代码中,a 是 add 的参数(当函数中局部变量与全局变量重名时,使用的是局部变量)
referencePaths 假设标识符被引用,referencePaths 中会存放所有引用该标识符的节点的 Path 对象
constantViolations 假如标识符被修改,那么 constantViolations 中会存放所有修改该标识符的节点的 Path 对象1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 traverse (ast, { FunctionExpression (path) { let bindingA = path.scope .getBinding ('a' ); let bindingDemo = path.scope .getBinding ('demo' ); console .log (bindingA.referenced ); console .log (bindingA.references ); console .log (generator (bindingA.scope .block ).code ); console .log (generator (bindingDemo.scope .block ).code ); }, })
3.2.5 scope.getOwnBinding scope.getOwnBinding
该函数用于获取当前节点自己的绑定,也就是不包含父级作用域中定义的标识符的绑定,但是该函数会得到子函数中定义的标识符的绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function TestOwnBinding (path ) { path.traverse ({ Identifier (p) { let name = p.node .name ; console .log (name, !!p.scope .getOwnBinding (name)); } }); } traverse (ast, { FunctionExpression (path) { TestOwnBinding (path); }, })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function TestOwnBinding (path ) { path.traverse ({ Identifier (p) { let name = p.node .name ; let binding = p.scope .getBinding (name); binding && console .log (name, generator (binding.scope .block ).code === path.toString ()); } }); } traverse (ast, { FunctionExpression (path) { TestOwnBinding (path); }, })
3.2.6 scope.traverse scope.traverse
方法可以用来遍历作用域中的节点。可以使用 Path
对象中的 scope
,也可以使用 Binding
中的 scope
,笔者推荐使用后者,下面来看下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 traverse (ast, { FunctionDeclaration (path) { let binding = path.scope .getBinding ('a' ); binding.scope .traverse (binding.scope .block , { AssignmentExpression (p) { if (p.node .left .name === 'a' ) { p.node .right = types.numericLiteral (500 ); } }, }) } })
3.2.7 scope.rename 可以使用 scope.rename
将标识符进行重命名,这个方法会同时修改所有引用该标识符的地方,例如将 add
函数中的 b
变量重命名为 x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 traverse (ast, { FunctionExpression (path) { let binding = path.scope .getBinding ('b' ); binding.scope .rename ('b' , 'x' ); }, })
上述方法很方便的就把 b
改为了 x
,但是如果随便指定一个变量名,可能会与现有标识符发生命名冲突,这时可以使用 scope.generateUidIdentifier
来生成一个标识符,生成的标识符不会与任何本地的标识符相冲突,代码如下:
1 2 3 4 5 6 7 8 9 traverse (ast, { FunctionExpression (path) { path.scope .generateUidIdentifier ('uid' ); path.scope .generateUidIdentifier ('_uid2' ); }, })
3.2.8 scope.hasBinding 该方法查询某标识符是否有绑定,返回 true
或者 false
。可以用 scope.getBinding("a")
代替,scope.getBinding("a")
返回 undefined
,等同于 scope.hasBinding("a")
返回 false
。
3.2.9 scope.hasOwnBinding 该方法查询当前节点中是否有自己的绑定,返回布尔值,例如,对于 demo
函数,OwnBinding
只有一个 d
,函数名 demo
虽然也是标识符,但不属于 demo
函数的 OwnBinding
范畴,是属于它的父级作用域中的,如:
1 2 3 4 5 6 7 8 9 10 traverse (ast, { FunctionDeclaration (path) { console .log (path.scope .parent .hasOwnBinding ('demo' )); }, })
3.2.10 scope.getAllBindings 该方法获取当前节点的所有绑定,会返回一个对象。该对象以标识符名为属性名,对应的 Binding
为属性值,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 traverse (ast, { FunctionDeclaration (path) { console .log (path.scope .getAllBindings ()); } })
遍历每一个 Binding
,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 traverse (ast, { BlockStatement (path) { console .log ('\n此块节点源码: \n' , path.toString ()); let bindings = path.scope .bindings ; console .log ('作用域内被绑定数量:' , Object .keys (bindings).length ); for (const bindingsKey in bindings) { console .log ('名字' , bindingsKey); let binding_ = bindings[bindingsKey]; console .log ('类型:' , binding_.kind ); console .log ('定义:' , binding_.identifier ); console .log ('是否常量:' , binding_.constant ); console .log ('被修改信息记录:' , binding_.constantViolations ); console .log ('是否被引用:' , binding_.referenced ); console .log ('被引用次数:' , binding_.references ); console .log ('被引用信息NodePath记录' , binding_.referencePaths ); } console .log ('-------------------------------------' ); }, })
3.2.11 scope .hasReference scope.hasReference(“a”) 表示查询当前节点中是否有 a 标识符的引用,返回布尔值。
3.2.12 scope .getBindingIdentifier scope.getBindingIdentifier(“a”) 表示获取当前节点中绑定的 a 的标识符,返回 Identifier 的 Node 对象。同样,这个方法也有 Own 版本,为 scope.getOwnBindingIdentifier(“a”)。
3.3 node 属性:
path.node.type 获取当前节点的类型。
path.node.declarations 对于 VariableDeclaration 节点, 获取变量声明列表。
path.node.init.value 获取某个节点的值。
delete path.node.init; 删除节点,使用系统的 delete 方法。
3.4 types 类型 3.5 节点类型对照
Program 程序主体 整段代码的主体
VariableDeclaration 变量声明 声明一个变量,例如 var let const
FunctionDeclaration 函数声明 声明一个函数,例如 function
ExpressionStatement 表达式语句 通常是调用一个函数,例如 console.log()
BlockStatement 块语句 包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
BreakStatement 中断语句 通常指 break
ContinueStatement 持续语句 通常指 continue
ReturnStatement 返回语句 通常指 return
SwitchStatement Switch 语句 通常指 Switch Case 语句中的 Switch
IfStatement If 控制流语句 控制流语句,通常指 if(condition){}else{}
Identifier 标识符 标识,例如声明变量时 var identi = 5 中的 identi
CallExpression 调用表达式 通常指调用一个函数,例如 console.log()
BinaryExpression 二进制表达式 通常指运算,例如 1+2
MemberExpression 成员表达式 通常指调用对象的成员,例如 console 对象的 log 成员
ArrayExpression 数组表达式 通常指一个数组,例如 [1, 3, 5]
NewExpression New 表达式 通常指使用 New 关键词
AssignmentExpression 赋值表达式 通常指将函数的返回值赋值给变量
UpdateExpression 更新表达式 通常指更新成员值,例如 i++
Literal 字面量 字面量
BooleanLiteral 布尔型字面量 布尔值,例如 true false
NumericLiteral 数字型字面量 数字,例如 100
StringLiteral 字符型字面量 字符串,例如 vansenb
SwitchCase Case 语句 通常指 Switch 语句中的 Case
SequenceExpression 一个序列表达式,也就是由逗号分割的表达式序列
FunctionExpression 函数赋值语句
4 常用技巧 4.1 基础使用框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const fs = require ('fs' );var util = require ('util' );const parser = require ('@babel/parser' );const traverse = require ('@babel/traverse' ).default ;const types = require ('@babel/types' );const generator = require ('@babel/generator' ).default ;var time_start = new Date ().getTime ()process.argv .length > 2 ? encode_file = process.argv [2 ] : encode_file = 'encode.js' ; process.argv .length > 3 ? decode_file = process.argv [3 ] : decode_file = 'decode.js' ; let jscode = fs.readFileSync (encode_file, {encoding : 'utf-8' });let ast = parser.parse (jscode);const visitor = { ASTNodeTypeHere (path, state) { } } traverse (ast, visitor);console .log ('AST traverse completed.' )let {code} = generator (ast);console .log ('AST generator completed.' )fs.writeFile (decode_file, code, (err ) => { }); console .log (util.format ('The javascript code in [%s] has been processed.' , encode_file))console .log (util.format ('The processing result has been saved to [%s].' , decode_file))var time_end = new Date ().getTime ()console .log (util.format ('The program runs to completion, time-consuming: %s s' , (time_end - time_start) / 1000 ))
4.2 打印原来的节点的源码 toString() 1 2 3 4 5 6 7 var js = 'var a = 1' const visitor = { VariableDeclarator (path){ console .log (path.toString ()); console .log (generator (path.node ).code ) }}
4.3 构造节点 t.valueToNode() (可以代替t.NumericLiteral(234567),t.StringLiteral(‘1234’) 注意:使用这两种过传参类型必须和声明类型一样,如果使用 valueNode 的话就是传入参数的默认类型)
1 2 3 4 5 6 { VariableDeclarator (path){ console .log (t.valueToNode ('123' )); console .log (t.valueToNode (123 )); console .log (t.valueToNode (null )) }}
4.4 替换节点 replaceWith 1 2 3 4 5 { 'VariableDeclarator' (path){ console .log (path.toString ()); path.replaceWith (t.valueToNode (1 )) }}
4.5 删除节点 remove() 1 2 3 4 5 6 { 'VariableDeclarator' (path){ console .log (path.toString ()) if (path.node .id .name === 'b' ){ path.remove ()}; }}
4.6 判断节点类型 isVariableDeclarator 1 2 3 4 5 6 7 { 'VariableDeclarator' (path){ console .log (path.toString ()) console .log (path.type ); console .log (t.isVariableDeclarator (path.node )) console .log (path.isVariableDeclarator ()) }}
4.7 节点设置值 set(key,node) 1 2 3 4 5 6 7 { 'VariableDeclarator' (path){ console .log (path.toString ()) const {init} = path.node console .log (init === path.node .init ) init || path.set ('init' , t.Identifier ("1" )) }}
4.8 插入节点 1 2 3 4 5 6 { 'Identifier' (path) { path.insertBefore (t.valueToNode ('22' )); path.insertAfter (t.valueToNode ('22' )); } }
5 高级技巧 5.1 执行 path 使用 path.evaluate()
,返回的 confident
为 true
则表明成功执行,value
为返回值 示例:
1 let { confident, value } = path.evaluate();
5.2 多个节点组合查询 使用 |
把需要访问的节点组合起来即可,例如:
1 2 3 4 const visitor = { 'VariableDeclarator|FunctionDeclaration' (path) {} }; traverse (ast, visitor);
5.3 删除无用的空语句 删除空语句 ;
1 2 3 4 5 6 const visitor ={ 'EmptyStatement' (path) { path.remove (); } }
5.4 删除无用节点 `path.remove()
5.5 查看作用域 scope path.scope.dump()
即可查看自底向上的作用域
5.6 同一节点使用多个函数 1 2 3 4 5 6 7 8 9 10 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] } } traverse (ast, visitor);
请注意! enter 需为数组!函数执行顺序为列表中函数顺序!
5.7 删除未使用的 function、var、let、const 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const visitor ={ VariableDeclarator (path) { const { id } = path.node ; let binding = path.scope .getBinding (id.name ); if (binding.referenced ) { return ; } path.remove (); path.scope .crawl (); }, FunctionDeclaration (path) { const { id } = path.node ; let binding = path.scope .parent .getBinding (id.name ); if (binding.referenced ) { return ; } path.remove (); path.scope .crawl (); } }
5.8 a[‘bb’] 转换为 a.bb 1 2 3 4 5 6 7 8 9 10 11 12 const visitor ={ MemberExpression (path) { let { computed } = path.node ; let property = path.get ('property' ); if (computed && types.isStringLiteral (property)) { property.replaceWith (types.identifier (property.node .value )); path.node .computed =false ; } } }
5.9 简单控制流 待处理代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var arr = "3|0|1|2|4" .split ("|" );var cnt = 0 ;while (true ) { switch (arr[cnt++]) { case "0" : console .log ("This is case-block 0" ); continue ; case "1" : console .log ("This is case-block 1" ); continue ; case "2" : console .log ("This is case-block 2" ); continue ; case "3" : console .log ("This is case-block 3" ); continue ; case "4" : console .log ("This is case-block 4" ); continue ; } break ; }
简单还原后的结果为:
1 2 3 4 5 console.log ("This is case-block 3" ); console.log ("This is case-block 0" ); console.log ("This is case-block 1" ); console.log ("This is case-block 2" ); console.log ("This is case-block 4" );
插件为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 'WhileStatement' (path){ const {test, body} = path.node ; if (!t.isBooleanLiteral (test) || test.value !== true ){return }; if (body.body .length === 0 || !t.isSwitchStatement (body.body [0 ])) {return }; let switch_state = body.body [0 ]; let {discriminant, cases} = switch_state; if (!t.isMemberExpression (discriminant) || !t.isUpdateExpression (discriminant.property )){return }; let arr_name = discriminant.object .name ; let arr = []; let all_pre_siblings = path.getAllPrevSiblings (); if (all_pre_siblings.length !== 2 ){return }; all_pre_siblings.forEach (pre_pth => { const {declarations} = pre_pth.node ; let {id, init} = declarations[0 ]; if (arr_name == id.name ){ arr = init.callee .object .value .split ('|' ); } pre_pth.remove (); }); let ret_body = []; arr.forEach (index => { let case_body = cases[index].consequent ; if (t.isContinueStatement (case_body[case_body.length -1 ])) {case_body.pop ()} ret_body = ret_body.concat (case_body) }) path.replaceInline (ret_body) }
5.10 自执行函数替换(固定函数参数) 待处理代码:
1 !function (a,b ) {c = a | b;}(111 ,222 );
简单还原后的结果为:
1 !function ( ) {c = 111 | 222 }();
插件为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 CallExpression(path) { let callee = path.get ('callee' ); let arguments = path.get ('arguments' ); if (!callee.isFunctionExpression() || arguments.length === 0 ) { return ; } let params = callee.get ('params' ); let scope = callee.scope; for (let i = 0 ; i < arguments.length; i++) { let arg = params [i]; let { name } = arg.node; const binding = scope.getBinding(name); console.log(binding) if (!binding || binding.constantViolations.length > 0 ) { continue ; } for (refer_path of binding.referencePaths) { refer_path.replaceWith(arguments[i]); } arg.remove (); arguments[i].remove (); } },
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 dacker1993@gmail