Proxy与Reflect嗅探缺失环境

一、前言

1.1 为什么需要补环境

同样一份js代码,在浏览器环境中可以执行出结果,在本地node环境却报错或者运行的结果是错的,将node环境补充到与浏览器环境相似到可以正确执行代码的程度,这种技术就叫做补环境。

浏览器环境:是指js代码在浏览器中的运行时环境,包括v8引擎、BOM和DOM三个部分。
node环境::是指js代码在node中的运行时环境,包括v8引擎、node内置api。

所以,对于网站而言,通过检测这两个环境之间的差异,可以有效的保护自己的js代码,例如:

function decrypt() {
    document = false;
    var flag = document?true:false;
    if (flag) {
        return "正确加密"
    } else {
        return "错误加密";
    }
}
/*
在浏览器环境运行时 flag为true,然后得到正常结果;
在Node环境运行时 flag为false,然后得到错误结果;
*/

但事实上并不总是这么简单,在补环境的过程中我们总能碰到一些问题,大概分为2种情况:

  1. 不知道缺失什么环境,例如:

  2. 补的环境不对,例如明明代码跑通了,没有报错,但为什么带入请求还是不通过,或者与浏览器执行的结果不一致。

那么有什么可以嗅探环境的办法吗?有,用Proxy。

1.2 Proxy作用

Proxy是一个类,是用于帮助我们创建一个代理对象,如果我们需要监听对象的操作,那么我们可以通过Proxy先创建一个代理对象,之后对该对象的所有操作都通过代理来完成。

Proxy和Object.defineproperty有相似的地方,例如都有代理的作用,但为什么这里要用proxy呢?因为defineproperty主要是用来代理属性的取值和赋值的行为,而Proxy是用来代理对象的行为的,我们的目标是找出什么对象缺失了什么属性,如果我们已经知道什么对象缺失了什么属性还有代理的必要吗?

在官方文档里,Proxy可以代理对象的行为高达13种,例如get,对象只要访问属性就会触发,此时我们代理这个对象的get行为,就能知道它在取什么属性,这么说有点抽象,来举一个例子吧。

上图中的代码报错提示undefined没有一个叫toString的属性,如果我们要补环境,那么要知道两个信息,哪个对象没有哪个属性,很遗憾,在这段报错信息中什么也得不到,虽然通过原代码我们知道是obj对象没有b这个属性,但真实情况是原代码经过了高度混淆,变的连亲妈都不认识了,现在我们来加上代理。

上图中,通过代理配置,我们成功知道要补的环境是obj对象的b属性,然后去浏览器中用obj.b提取一下再补充进来就好了。

接下来我们通过一系列小的案例,来制作Proxy对象,代理目标对象的各种行为。

二、方法

2.1 代理get/set行为

案例:

let obj = { 'a': { 'b': 1 } }
console.log(obj['a']['c'].toString())

输出:

代理:

let proxy = function (obj, objname) {
    let handle = {
        get: function (target, prop, receiver) {
            let result;
            try {
                result = Reflect.get(target, prop, receiver);
                let type = Object.prototype.toString.call(result)
                if (result instanceof Object) {
                    console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],type:[${type}]}`);
                    //递归代理
                    result = proxy(result, `${objname}.${prop.toString()}`);
                } else if (typeof result === 'symbol') {
                    console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result.toString()}]}`);
                } else {
                    console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result}]}`);
                }
            } catch (e) {
                console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
            }
            return result
        },
        set: function (tarset, prop, receiver) {
            let result;
            try {
                result = Reflect.set(tarset, prop, receiver);
                let type = Object.prototype.toString.call(result)
                if (result instanceof Object) {
                    console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],type:[${type}]}`);
                } else if (typeof result === 'symbol') {
                    console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result.toString()}]}`);
                } else {
                    console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result}]}`);
                }
            } catch (e) {
                console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
            }
            return result
        }

    }
    return new Proxy(obj, handle)
}

结果:

从图中可以看出,缺失的环境是obj对象下的a对象下的c属性,由于没有c属性,所以调用toString()会报这样的错误。

简单说下流程,定义了一个proxy函数,内部返回一个Proxy对象,需要传递obj,以及obj对象的名称,在函数内部代理obj的get行为,根据get行为执行的结果类型,输入不同的日志,如果返回结果类型是Object类型,那么需要递归代理,如果返回是是Symbol类型,需要用ToString()方法将其转为字符串输出(因为模版字面量无法拼接Symbol类型,会报错Symobol无法转字符串),如果其它类型,则直接输出这个结果。最后还要增强鲁棒性,所以再套一层try…catch..。

set行为与get类似,但无需递归调用,因为set行为一般不会报错(给一个对象设置一个不存在的属性本身就是一件合理的事),代理set行为在这里更多的意义是通过日志输出帮助分析代码执行流程。

:为什么要用Object.prototype.toString.call来判断类型,而不用typeof 或者 instanceof?

首先instanceof是判断某个对象是不是另外一个对象的实例,在这里结果是未知的,所以没有先决条件,其次,假如返回结果是null,用typeof得到的就是object,而用Object.prototype.toString.call能够判断出来这是null类型,也就是说判断更为精准,在日志输出中我们更像知道这是null,而不是object这种略显宽泛的类型。

let a = null

console.log(Object.prototype.toString.call(a))
console.log(typeof a)
console.log(a instanceof Object)
/*
[object Null]
object
false
*/

2.2 代理getOwnProperty/defineProperty行为

案例:

let obj = { 'a': 1 }
console.log(Object.getOwnPropertyDescriptor(obj, 'b').toString())

输出:

代理:

let handle = {
    getOwnPropertyDescriptor: function (target, prop) {
        let result;
        try {
            result = Reflect.getOwnPropertyDescriptor(target, prop);
            let type = Object.prototype.toString.call(result)
            console.log(`{getOwnPropertyDescriptor|obj:[${objname}] -> prop:[${prop.toString()}],type:[${type}]}`);
        } catch (e) {
            console.log(`{getOwnPropertyDescriptor|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
        }
        return result;
    },
    defineProperty: function (target, prop, descriptor) {
        let result;
        try {
            result = Reflect.defineProperty(target, prop, descriptor);
            console.log(`{defineProperty|obj:[${objname}] -> prop:[${prop.toString()}]}`);
        } catch (e) {
            console.log(`{defineProperty|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
        }
        return result;
    }
}

结果:

从图中可以看出,缺失的环境是obj对象下的b对象下的属性。

容易看出,getOwnPropertyDescriptor行为和get行为都是为了访问某个对象成员,虽然实际情况用get行为访问更常见,但为了反爬,对方就是用getOwnPropertyDescriptor行为访问成员,那我们也只能把这种情况考虑进去,日志中正常输入调用方对象和访问属性名是没有问题,结果中只记录类型,而不是具体的值,是因为这种行为的结果只有两种可能,一种是object,一种是undefined,而对于object对象没有很好的办法打印出来,所以只记录一个类型,当为undefined的时候,我们就知道需要补了。

对于defineProperty行为其实与set行为类似,基本上不会报错,因此更多的意义是帮助分析代码执行流程。

2.3 代理apply行为

我们都知道js中函数也是对象,所以完全可以代理。
案列:

let a = {}
console.log(a['b'].apply())

输出:

代理:

let handle = {
    apply: function (target, thisArg, argumentsList) {
        let result;
        try {
            result = Reflect.apply(target, thisArg, argumentsList)
            let type = Object.prototype.toString.call(result)
            if (result instanceof Object) {
                console.log(`{apply|function:[${objname}],type:[${type}]}`);
            } else if (typeof result === "symbol") {
                console.log(`{apply|function:[${objname}] -> result:[${result.toString()}]}`);
            } else {
                console.log(`{apply|function:[${objname}] -> result:[${result}]}`);
            }
        } catch (e) {
            console.log(`{apply|function:[${objname}] ,error:[${e.message}]}`);
        }
        return result;
    }
}

结果:

从图中可以看出,缺失的环境是obj对象下的b对象。

如果apply执行结果是object类型,那么就输出这个类型,如果是symbol类型,就用toString转字符串输出,如果是其它类型,就直接输出。

2.4 其它代理行为

let handle = {
    // 构造器,new xx时触发
    construct: function (target, argArray, newTarget) {
        let result;
        try {
            result = Reflect.construct(target, argArray, newTarget);
            let type = Object.prototype.toString.call(result)
            console.log(`{construct|function:[${objname}], type:[${type}]}`);
        } catch (e) {
            console.log(`{construct|function:[${objname}],error:[${e.message}]}`);
        }
        return result;

    },
    // 删除对象属性时触发
    deleteProperty: function (target, propKey) {
        let result = Reflect.deleteProperty(target, propKey);
        console.log(`{deleteProperty|obj:[${objName}] -> prop:[${propKey.toString()}], result:[${result}]}`);
        return result;
    },
    // in 操作符 
    has: function (target, propKey) { 
        let result = Reflect.has(target, propKey);
        console.log(`{has|obj:[${objName}] -> prop:[${propKey.toString()}], result:[${result}]}`);
        return result;
    },
    // 遍历对象属性时触发
    ownKeys: function (target) {
        let result = Reflect.ownKeys(target);
        console.log(`{ownKeys|obj:[${objName}]}`);
        return result
    },
    // 获取对象原型对象时触发
    getPrototypeOf: function (target) {
        let result = Reflect.getPrototypeOf(target);
        console.log(`{getPrototypeOf|obj:[${objName}]}`);
        return result;
    },
    // 设置对象原型时触发
    setPrototypeOf: function (target, proto) {
        let result = Reflect.setPrototypeOf(target, proto);
        console.log(`{setPrototypeOf|obj:[${objName}]}`);
        return result;
    }
}

其余代理行为一般不会报错,但亦可作为日志帮助分析代码逻辑。

三、总结

为了能够在补环境时快速的嗅探出缺失环境,本文采用了Proxy代理对象的各种行为,并对常见的行为做了举例分析,有get/set,getOwnProperty/defineProperty和apply等,避免了由于环境差异导致浏览器端扣下来的代码在本地node环境运行时报错,但又无法快速定位到是哪一个环境缺失,最后将这些代理封装成函数,方便传入对象,进行快速代理,即可单独使用,也可作为补环境框架的组件。(完整代码见文末附录)

四、附录

let proxy = function (obj, objname) {
    let handle = {
        get: function (target, prop, receiver) {
            let result;
            try {
                result = Reflect.get(target, prop, receiver);
                let type = Object.prototype.toString.call(result)
                if (result instanceof Object) {
                    console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],type:[${type}]}`);
                    //递归代理
                    // result = proxy(result, `${objname}.${prop.toString()}`);
                } else if (typeof result === 'symbol') {
                    console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result.toString()}]}`);
                } else {
                    console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result}]}`);
                }
            } catch (e) {
                console.log(`{get|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
            }
            return result
        },
        set: function (tarset, prop, receiver) {
            let result;
            try {
                result = Reflect.set(tarset, prop, receiver);
                let type = Object.prototype.toString.call(result)
                if (result instanceof Object) {
                    console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],type:[${type}]}`);
                } else if (typeof result === 'symbol') {
                    console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result.toString()}]}`);
                } else {
                    console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],ret:[${result}]}`);
                }
            } catch (e) {
                console.log(`{set|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
            }
            return result
        },
        getOwnPropertyDescriptor: function (target, prop) {
            let result;
            try {
                result = Reflect.getOwnPropertyDescriptor(target, prop);
                let type = Object.prototype.toString.call(result)
                console.log(`{getOwnPropertyDescriptor|obj:[${objname}] -> prop:[${prop.toString()}],type:[${type}]}`);
            } catch (e) {
                console.log(`{getOwnPropertyDescriptor|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
            }
            return result;
        },
        defineProperty: function (target, prop, descriptor) {
            let result;
            try {
                result = Reflect.defineProperty(target, prop, descriptor);
                console.log(`{defineProperty|obj:[${objname}] -> prop:[${prop.toString()}]}`);
            } catch (e) {
                console.log(`{defineProperty|obj:[${objname}] -> prop:[${prop.toString()}],error:[${e.message}]}`);
            }
            return result;
        },
        apply: function (target, thisArg, argumentsList) {
            let result;
            try {
                result = Reflect.apply(target, thisArg, argumentsList)
                let type = Object.prototype.toString.call(result)
                if (result instanceof Object) {
                    console.log(`{apply|function:[${objname}],type:[${type}]}`);
                } else if (typeof result === "symbol") {
                    console.log(`{apply|function:[${objname}] -> result:[${result.toString()}]}`);
                } else {
                    console.log(`{apply|function:[${objname}] -> result:[${result}]}`);
                }
            } catch (e) {
                console.log(`{apply|function:[${objname}] ,error:[${e.message}]}`);
            }
            return result;
        },
        construct: function (target, argArray, newTarget) {
            let result;
            try {
                result = Reflect.construct(target, argArray, newTarget);
                let type = Object.prototype.toString.call(result)
                console.log(`{construct|function:[${objname}], type:[${type}]}`);
            } catch (e) {
                console.log(`{construct|function:[${objname}],error:[${e.message}]}`);
            }
            return result;

        },
        deleteProperty: function (target, propKey) {
            let result = Reflect.deleteProperty(target, propKey);
            console.log(`{deleteProperty|obj:[${objName}] -> prop:[${propKey.toString()}], result:[${result}]}`);
            return result;
        },
        has: function (target, propKey) { // in 操作符
            let result = Reflect.has(target, propKey);
            console.log(`{has|obj:[${objName}] -> prop:[${propKey.toString()}], result:[${result}]}`);
            return result;
        },
        ownKeys: function (target) {
            let result = Reflect.ownKeys(target);
            console.log(`{ownKeys|obj:[${objName}]}`);
            return result
        },
        getPrototypeOf: function (target) {
            let result = Reflect.getPrototypeOf(target);
            console.log(`{getPrototypeOf|obj:[${objName}]}`);
            return result;
        },
        setPrototypeOf: function (target, proto) {
            let result = Reflect.setPrototypeOf(target, proto);
            console.log(`{setPrototypeOf|obj:[${objName}]}`);
            return result;
        },

    }
    return new Proxy(obj, handle)
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,如有问题请邮件至2454612285@qq.com。
跃迁主页