相信很多人对 ES6 中的 Proxy 有所了解,Proxy 的所有用法,都是下面这种形式:
let proxy = new Proxy(target, handler);
其中,new Proxy() 表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。
new Proxy(...) instanceof Proxy? No
那么大家有没有想过这么一个问题?怎样去判断某个变量是一个 Proxy 实例呢?
大家可能都能想到用 instanceof
操作符,下面我们就来尝试一下:
let arr = []
let proxy = new Proxy(arr, {});
在这里,我们定义了一个数组 arr,接着通过 Proxy 对 arr 做了一层拦截,生成了一个实例 proxy。我们通过 proxy instanceof Proxy
来判断一下 proxy 的原型上有没有 Proxy 这个构造函数。
proxy instanceof Proxy
//Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof check at Function.[Symbol.hasInstance] (<anonymous>) at <anonymous>:1:7
答案出乎意料,既不是 true 也不是 false 而是报错了。
我们再看一下 proxy 的类型是什么,这里我们用 Object.prototype.toString.call(xx)
Object.prototype.toString.call(proxy)
//"[object Array]"
proxy instanceof Array
//true
proxy.__proto__.constructor
//ƒ Array() { [native code] }
由此可见,new Proxy() 生成的实例和被代理的 target 对象始终保持着相同的引用类型。Proxy 实例的原型上也没有所谓的 Proxy 构造函数。
可以理解,Proxy 这样设计的目的,就是单纯地给一个 target 加上一层拦截,返回的就是一个修改了默认运行机制的 target 对象。而 target 是什么类型,返回的代理就是什么类型。而这样的设计,看起来似乎让刚才抛出的问题无解,就是我们可能无法通过常规的方式判断一个变量是一个 Proxy 实例了。
直到我看到了 Symbol.toStringTag
有一个和 Proxy 一样属于元编程范畴的 Symbol,是ES6 中新引入的原始数据类型。Symbol 值通过 Symbol 函数生成。除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
其中有一个是 Symbol.toStringTag
,对象上可以设置一个 Symbol.toStringTag
属性,指向一个方法。在该对象上面调用 Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在 toString 方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制 [object Object] 或 [object Array] 中 object 后面的那个字符串。下面是几个例子:
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
由此可见,我们可以通过对象的 Symbol.toStringTag
属性来改写 Object.prototype.toString
方法对其执行的结果。
这个时候,我产生了一个想法,就是让所有的 Proxy 实例的Symbol.toStringTag
属性,都等于 'Proxy-'
加上原来的类型,例如之前的例子:
let arr = []
let proxy = new Proxy(arr, {});
Object.prototype.toString.call(proxy)
//"[object Array]"
实际的结果是 [object Array],但我希望它是 [object Proxy-Array],表示它是一个被代理过一次的 Array。
这就要让所有 Proxy 实例的Symbol.toStringTag
属性自动被改写,加上'Proxy-',如何达到这个目的呢?下面揭晓答案。
解决方法
Proxy 可代理的 target 类型很多,其中包括函数 function。而 hanlder 中有一个属性 handler.construct()
方法用于拦截 new 操作符,即拦截 target 作为一个构造函数生成一个实例的操作new target()
。
于是我突发奇想,Proxy 本身也是一个构造函数,那我可不可以通过 Proxy 代理 Proxy 这个构造函数本身,来改变 Proxy 生成的实例,从而改写实例的Symbol.toStringTag
属性呢?答案是肯定的,下面看代码和注释:
//通过 Proxy 对 Proxy 本身做代理,然后赋值给 Proxy
Proxy = new Proxy(Proxy, {
//拦截 new 操作符,生成 Proxy 实例的时候来拦截
construct: function (target, argumentsList) {
//result是new Proxy()生成的原本的实例
const result = new target(...argumentsList);
//获取原本实例reslut的类型
const originToStringTag = Object.prototype.toString.call(result).slice(1,-1).split(' ')[1]
//改写result的[Symbol.toStringTag]属性,加上被代理的标志
result[Symbol.toStringTag] = 'Proxy-' + originToStringTag;
return result;
},
});
在这里,Proxy 本身已经被改写,由它生成的实例,在执行 Object.prototype.toString.call()
时,都会自动加一个 'Proxy-'
字符串。
来验证一下效果:
let a = new Proxy([],{})
//通过Object.prototype.toString.call方法获取a的类型
Object.prototype.toString.call(a)
//"[object Proxy-Array]" 达到效果,表示a是一个被代理过一次的Array
let b = new Proxy({},{})
//通过Object.prototype.toString.call方法获取b的类型
Object.prototype.toString.call(b)
//"[object Proxy-Object]" 达到效果,表示b是一个被代理过一次的Object
let c = new Proxy(function(){},{})
//通过Object.prototype.toString.call方法获取c的类型
Object.prototype.toString.call(c)
//"[object Proxy-Function]" 达到效果,表示c是一个被代理过一次的Function
//继续对a做代理,赋值给d
let d = new Proxy(a,{})
//通过Object.prototype.toString.call方法获取d的类型
Object.prototype.toString.call(d)
//"[object Proxy-Proxy-Array]" 达到效果,表示d是一个被代理过两次的Array
由此可见,结合 Proxy 和 Symbol 的使用,可以成功地判断一个引用类型变量是 Proxy 的实例。
总结
Proxy 很强大,可以代理各种引用类型,包括它自身。而一些内置的 Symbol 值,又指向语言内部使用的一些方法。
Proxy 和 Symbol 都属于元编程的范畴,让你具备一定程度上改变现有程序规则层面的能力,这就让编码变得无限可能。而更多元编程的用法,还有待大家进一步探索,探索的过程是很有趣的。