# AST 基本命令
# path
path 为 AST 解析后的产物,在解析时,先将所有代码解析为一个大 file, 其中拥有 path 属性,若是针对一种表达式解析 (例如 VariableDeclarator 赋值表达式),那么 path 指代的就为赋值表达式所在的路径,其中又包含很多属性,主要的有:
- type: 表示表达式类型
- node 表示该节点、为一个对象、包含很多属性
- parent: 该路径的父路径,这里指代的是 node 类型,若要获取 path 类型需要 path.parentPath.
如 var a = b; 那么 a=b 为赋值表达式当前路径、var a = b 为父路径。
- scope: 作用域
path.toString () 可以打印出当前代码位置.
# node
node 本质上是一个对象,内置属性如下 (仍以 VariableDeclarator 为例):
- type: 表示表达式类型
- start: 开始位置
- end: 结束位置
- id: 对应左半部分,也为节点类型
- init: 对应右半部分,也为节点类型
node 可以采用 generator (path.node).code 的方式获取当前代码位置.
# binding
绑定,为 scope 中的属性,要使用 path.scope.getBinding (name) 得到,其中 name 为想要绑定对象昵称。
block: 指代整体 path?
binding.constant:Boolean (表示是否被修改过值),constantViolations 表示修改值的位置。
referencePaths: 绑定对象所在区域,例如:
var a = 1; | |
var b = a + 1; |
在对第一个表达式遍历的时候,a 对应的 referencePaths 为 var b = a+ 1 其中的 a,即我们可以通过该方式替换节点的值,达到批量修改的目的。
# 浅层检查
t.isIdentifier(path.node.left, { name: "n" }) | |
等价于 | |
path.node.left != null && | |
path.node.left.type === "Identifier" && | |
path.node.left.name === "n" |
# get 方法
使用 path.get ('target'),可以获得对应的 path 路径,相比于直接获取 node 节点,在确定作用域的时候更有效,比如:
(function(a,b){console.log(a+b);}()); | |
// 对于上述自执行函数,我们通过遍历 CallExpression 节点,然后通过 path.scope.getBinding ('a') 的方法只能获取到 undefined, 这是由于,当前的 path 指代的是外部作用域,不能指向自执行函数内部,因此,想要获取到 'a' 的 binding, 就需要通过 path.get ('callee') 的方法,获取内部函数的作用域,这样一来就可以通过遍历的方式 j |
# 插件优化
# 变量替换
目的:将赋值表达式值为字面量、数字的直接批量替换
const visitor = { | |
VariableDeclarator(path){ | |
const {id,init} = path.node; | |
// 获取对应变量的绑定 | |
var binding = path.scope.getBinding(id.name); | |
// 获取绑定作用域 | |
referpaths = binding.referencePaths; | |
// 如果变量没引用,则返回 | |
if(referpaths.length === 0) return; | |
// 保证变量的 init 部分值唯一,数字、字符类型直接替换,函数定义也可直接替换,前提是引用部分为单引用,若为函数调用,可考虑直接计算值。 | |
// 遍历绑定作用域 | |
for (referpath of referpaths){ | |
// 节点值替换 | |
referpath.replaceWith(path.node.init); | |
}; | |
// 节点删除 | |
path.remove(); | |
} | |
}; |
# 数组对象还原
/* | |
var b = ['asdad'].concat(function(){return arguments}); | |
var c = b[1]; | |
b.shift(); | |
var d = b[0]; | |
*/ | |
// 通过遍历声明节点,找到数组命名的位置,然后将其写入内存,接着在将 | |
const visitor12 = { | |
VariableDeclarator(path) { | |
const {id, init} = path.node; | |
var func_name = id.name; | |
var binding = path.scope.getBinding(func_name); | |
var referpaths = binding.referencePaths; | |
// 如果变量没引用,则返回 | |
if (referpaths.length === 0) return; | |
// 保证变量的 init 部分值唯一,数字、字符类型直接替换,函数定义也可直接替换,前提是引用部分为单引用,若为函数调用,可考虑直接计算值。 | |
// 单独针对这种情况,不考虑单纯的数组。 | |
if (types.isCallExpression(init) && types.isArrayExpression(init.callee.object)) { | |
// 将数组中的内容直接写入内存 | |
if(init.callee.property.name !== 'concat') return; | |
var old_arr = init.callee.object.elements.concat(init.arguments[0]); | |
global[func_name] = old_arr; | |
// 遍历作用域,获取修改值的地址 | |
binding.scope.traverse(binding.scope.block, { | |
MemberExpression(_path) { | |
// 后面可能还要确保只修改一次的情况 | |
console.log(_path.toString()); | |
if (_path.node.property.value >= 0) { | |
let index_value = _path.node.property.value; | |
_path.replaceInline(eval(func_name)[index_value]); | |
}; | |
// 若包含 shift 操作 | |
if (_path.node.property && _path.node.property.name === 'shift') { | |
global[func_name].shift() | |
} | |
} | |
}); | |
path.remove(); | |
} | |
} | |
} |
# 自执行函数还原
# 思路 1: 使用 referencePath 对其进行回填。
/* | |
(function (a,b){console.log (a+b)})(3,4)------->console.log (3+4) | |
自执行函数为 CallExpression 块,callee 指向 FunctionExpression, 需要做的就是将 block 块中提取,然后将变量值回填, | |
简单来讲,假如这个值没有被修改啥的,直接遍历即可 | |
*/ | |
(function(a,b){ | |
var arr = arguments; | |
if(a!==3) return; | |
for(var i=0;i<2;i++) { | |
arr[i] += i; | |
}; | |
a = a+1; | |
var c = a + b; | |
})(3,4); | |
------------------------------------- | |
// 自执行函数回填 | |
const self_func_replace = { | |
CallExpression(path) { | |
let callee = path.get('callee'); | |
const {arguments} = path.node; | |
if(!types.isFunctionExpression(callee.node)) return; | |
var {params, body} = callee.node; | |
// 遍历形参数组,进行回填 | |
for(var i=0;i<arguments.length;i++){ | |
binding = callee.scope.getBinding(params[i].name); | |
references = binding.referencePaths; | |
// 遍历当前形参的 Binding 节点,并替换。 | |
console.log(binding.constantViolations); | |
if(!binding || !binding.constant){ | |
continue; | |
}; | |
for(reference of references){ | |
reference.replaceInline(types.valueToNode(arguments[i].value)); | |
} | |
}; | |
} | |
}; | |
---------------------------------- | |
(function (a, b) { | |
var arr = arguments; | |
if (a !== 3) return; | |
for (var i = 0; i < 2; i++) { | |
arr[i] += i; | |
} | |
; | |
a = a + 1; | |
var c = a + 4; | |
})(3, 4); |
可以看到,由于 a 在作用域中的值被改变了,所以并没有进行回填。
但是,从实际上来看,这两个值其实都被修改了,所以说,要想用 referencePath 来对其自执行函数进行回填有一定的局限性。
既然我们知道了,是通过一个 for 循环来对传入的 arguments 进行的处理,那么,我们只要把这个 for 循环的算法封装成一个函数,直接输出一个新的 arguments 数组,这样一来,后面替换的时候,就采用新的数组替换不就完了么。
function get_arr_node(arguments){ | |
arr = []; | |
for(i of arguments){ | |
arr.push(i.value); | |
}; | |
// 对字符的一个处理 | |
for(var i=0;i<2;i++) { | |
arr[i] += i; | |
}; | |
return arr | |
}; | |
// 自执行函数回填 | |
const self_func_replace = { | |
CallExpression(path) { | |
let callee = path.get('callee'); | |
const {arguments} = path.node; | |
var new_arguments = get_arr_node(arguments); | |
if(!types.isFunctionExpression(callee.node)) return; | |
var {params, body} = callee.node; | |
// 遍历形参数组,进行回填 | |
for(var i=0;i<arguments.length;i++){ | |
binding = callee.scope.getBinding(params[i].name); | |
references = binding.referencePaths; | |
// 遍历当前形参的 Binding 节点,并替换。 | |
if(!binding || !binding.constant){ | |
continue; | |
}; | |
for(reference of references){ | |
reference.replaceInline(types.valueToNode(new_arguments[i])); | |
} | |
}; | |
} | |
}; | |
-------------------------------------------- | |
(function (a, b) { | |
if (a !== 3) return; | |
for (var i = 0; i < 2; i++) { | |
arguments[i] += i; | |
} | |
; | |
a = a + 1; | |
var c = a + 5; | |
})(3, 4); |
可以看到,这里已经成功的把 b 填进去,同时也为正确的数了,要做到完全还原,也只需要遍历 a 进行回填即可。