# 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 进行回填即可。
