怎样判断一个引用类型变量是 Proxy 的实例?

4,922 阅读5分钟

相信很多人对 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 都属于元编程的范畴,让你具备一定程度上改变现有程序规则层面的能力,这就让编码变得无限可能。而更多元编程的用法,还有待大家进一步探索,探索的过程是很有趣的。