阅读 824

将原型对象设置成Proxy后的一系列迷惑行为

前言

Proxy代理 相信大家或多或少都有所耳闻,这是一个ES6推出的新特性,能够拦截用户对对象的各种操作。

Vue3 也是用了 Proxy 代理替换掉了原来的 Object.defineProperty() ,不但解决了之前添加新属性不会触发响应式等bug,更是大幅度提高了性能。因为之前要靠 Object.defineProperty() 把用户定义在 data、methods、props、computed、watch、mixins… 里的一系列变量全都绑定在this上,所以一上来就是一顿遍历操作。

而且为了能够让最深层次的数据也具有响应式,如:

data () {
	return {
    	a: {
        	b:[{
            	c: [
                	[
                    	{
                        	d: {
                            	e: ['f']
                            }
                        }
                    ]
                ]
            }]
        }
    }
}
复制代码

可想而知遍历类似于这样层层嵌套的数据结构是有多么的耗时,这也就是为什么在数据量比较庞大的时候Vue的性能不如React的重要原因之一。

但 Proxy 很"聪明",假如有个深层次嵌套的数据结构,Proxy 可不是一上来就一顿无脑遍历,而是当你用到深层次嵌套结构里面数据的时候才会遍历到对应的层级。

为了能够看懂 Vue3 的源码,并希望从中能够学到一些可以用在业务开发的 Proxy 骚操作,我仔细的研究一下了 Proxy 的各种用法。在研究过程中我发现将原型对象设置成 Proxy 之后,产生了一系列十分让人迷惑的行为,先来看一下我是怎么发现的这些迷惑行为。

Proxy的常规用法

const target = {}

const obj = new Proxy(target, {
	get (target, propKey, receiver) {
    	// 拦截对象属性的读取,如 proxy.foo 和 proxy['foo']
    },
    
    set (target, propKey, value, receiver) {
    	// 拦截对象属性的设置,如 proxy.foo = v 或 proxy['foo'] = v
        // 返回一个布尔值
    },
    
    has (target, propKey) {
    	// 拦截propKey in proxy的操作,返回一个布尔值
    },
    
    deleteProperty (target, propKey) { 
    	// 拦截delete proxy[propKey]的操作,返回一个布尔值
    },
    
    ownKeys (target) { 
    	// 拦截Object.getOwnPropertyNames(proxy)、
        // 	Object.getOwnPropertySymbols(proxy)、
        //	Object.keys(proxy)以及for...in循环
        // 返回一个数组
        // 该方法返回目标对象所有自身的属性的属性名
        // 而Object.keys()的返回结果仅包括目标对象自身的可遍历属性
    },
    
    getOwnPropertyDescriptor (target, propKey) { 
    	// 拦截Object.getOwnPropertyDescriptor(proxy, propKey)
        // 返回属性的描述对象
    },
    
    defineProperty (target, propKey, propDesc) {
    	// 拦截Object.defineProperty(proxy, propKey, propDesc)、
        //	Object.defineProperties(proxy, propDescs)
        // 返回一个布尔值
    },
    
    preventExtensions (target) {
    	// 拦截Object.preventExtensions(proxy)
        // 返回一个布尔值
    },
    
    getPrototypeOf (target) {
    	// 拦截Object.getPrototypeOf(proxy)
        // 返回一个对象
    },
    
    setPrototypeOf (target, proto) {
    	// 拦截Object.setPrototypeOf(proxy, proto)
        // 返回一个布尔值
    },
    
    isExtensible (target) {
    	// 拦截Object.isExtensible(proxy)
        // 返回一个布尔值
    },
    
    apply (target, object, args) {
    	// 拦截 Proxy 实例作为函数调用的操作
        // 如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)等
    },
    
    construct (target, args) {
    	// 拦截 Proxy 实例作为构造函数调用的操作
        // 如new proxy(...args)
    }
})
复制代码

上面的所有属性都是可选的,来看一下在 TypeScript 中 Proxy 的声明:

interface ProxyHandler<T extends object> {
    getPrototypeOf? (target: T): object | null;
    setPrototypeOf? (target: T, v: any): boolean;
    isExtensible? (target: T): boolean;
    preventExtensions? (target: T): boolean;
    getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
    has? (target: T, p: PropertyKey): boolean;
    get? (target: T, p: PropertyKey, receiver: any): any;
    set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
    deleteProperty? (target: T, p: PropertyKey): boolean;
    defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
    enumerate? (target: T): PropertyKey[];
    ownKeys? (target: T): PropertyKey[];
    apply? (target: T, thisArg: any, argArray?: any): any;
    construct? (target: T, argArray: any, newTarget?: any): object;
}

interface ProxyConstructor {
    revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
    new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
复制代码

可以看到上面所有的属性都是可选的,也就是说哪怕写成:** new Proxy(target, {}) ** 都是没问题的。

当然,既然使用了 Proxy,就最好不要再操作原对象 target 了。可以理解为既然有了新女友,就不要再去联系前女友了。

为了防止你有时脑子一抽突然跑去联系"前女友 target",所以最好不要留下 target 的任何引用(最好不要留下前女友的任何联系方式):

const obj = new Proxy({}, {
	// 这里省略一系列操作
})
复制代码

这回没有target的任何引用了,所以根本找不到那个"对象"了,就只能操作"现女友 obj"了。

Proxy的非常规用法

既然 Proxy 可以代理对象,那么对象的原型不也是对象么。

JS的继承不就是靠的原型链一层层向上找,那假如我们想要获取一个原对象没有的属性,只要我们把它的 __proto__ 对象用 Proxy 代理一下,它就会向上找到这个代理对象,然后被代理对象所拦截。

既然想到了那就赶紧打开控制台试一下吧:

const obj = {}

Object.setPrototypeOf(obj, new Proxy(Object.getPrototypeOf(obj), {
	get (target, key, receiver) {
    	return '拦截到了访问__proto__原型对象的操作'
    }
}))

console.log(obj.a)
// 拦截到了访问__proto__原型对象的操作
复制代码

Object.getPrototypeOf() 和 Object.setPrototypeOf() 是ES6新增的方法

用来代替之前大家常用的 obj.__proto__obj.__proto__ = xxx
之所以需要用这个方法代替的原因是本来 __proto__ 并不是ES标准里面支持的属性
只不过由于浏览器的广泛支持证明了许多业务需要操作这个对象
但毕竟以双下划线开头和双下划线结尾的__xxx__属性是内部私有属性 不建议使用
所以 ES6 想出了这两个方法作为代替

如果上面的代码看起来不太容易理解的话我们可以将其换成下面这种代码:

const obj = Object.create(new Proxy({}, {
	get (target, key, receiver) {
    	return '拦截到了访问__proto__原型对象的操作'
    }
}))

console.log(obj.a)
// 拦截到了访问__proto__原型对象的操作
复制代码

可以看到在访问 obj 对象没有的 a 属性时,由于 JS 需要向原型链上继续查找的机制,会导致访问了位于原型对象上的 Proxy 代理,从而被拦截住 get 操作,返回了我们自己定义的字符串。

迷惑行为之调用对象时自动去原型找get操作

接下来我们就来改写一下 get 函数:

Object.setPrototypeOf(obj, new Proxy(Object.getPrototypeOf(obj), {
	get (target, key, receiver) {
        console.log('get')
    	return '拦截到了访问__proto__原型对象的操作'
    }
}))
复制代码

然后在控制台打印一下原对象 obj:

发现它虽然没有返回咱们自定义的那个字符串,但是居然打印了两次 get ,这是为什么呢?

我的猜测是在控制台里打印出 obj 对象时,控制台会访问 obj 的所有属性,这样才能够把它的所有展示给大家,这里当然也会包括 __proto__ :

迷惑行为之调用对象时自动去原型找splice

那么既然调用对象时会触发 get 方法,那么究竟是 get 了哪个属性呢?再来改写一下代码:

Object.setPrototypeOf(obj, new Proxy(Object.getPrototypeOf(obj), {
	get (target, key, receiver) {
        console.log(`get: ${key}`)
    	return '拦截到了访问__proto__原型对象的操作'
    }
}))
复制代码

splice ? 这让我感到更加迷惑了,splice不是数组的方法么?为什么要调用这个方法?

咱们再把原型改成普通对象,然后再添加一个splice,看看会不会调用它:

可以看到无论是把 splice 写成方法还是普通的属性,调用obj的时候都不会去原型找它。

而且直接创建一个空对象也找不到它的 splice。

迷惑行为之赋值也会被原型的Proxy所拦截

如果说取值行为是在自身找不到对应的属性就去原型链查找的话,那么赋值行为可不会去找原型链。

如果自身没有这个属性那就实打实的为自身添加一个:

const obj = Object.create({ prop: '__protp__' })

console.log(obj.prop) // __proto__


obj.proto = 'this'

console.log(obj.prop) // this
复制代码

可以看出自身的赋值操作压根就不会影响到原型对象上去,那么我们再把原型对象设置成 Proxy 代理试试:

const obj = Object.create(new Proxy({}, {
    set (obj, k, v) {
        console.log(`key: ${k}, value: ${v}`)
        return Reflect.set(obj, k, v)
    }
}))

obj.prop = 'this' // key: prop, value: this
复制代码

奇怪,我设置在对象本身上的属性怎么被原型对象给拦截了?打印一下对象看看:

原对象是空的,属性竟然设置到原型对象上去了!这还是头一次见识这种骚操作。

迷惑行为之有些选项内有console就会崩溃

const obj = Object.create(new Proxy({}, {
    ownKeys () { console.log('keys') }
}))
复制代码

只要你敢打开控制台输入obj这三个字母,页面立刻就会崩溃给你看:

类似的选项除了 ownKeys 以外还有:

  • getOwnPropertyDescriptor

在vue3中的Proxy

setup () {
  const target = reactive({})

  const obj = Object.create(target)
  obj.a = 1

  console.log(obj)
}
复制代码

可以看到在 vue 中把对象的原型设置成一个 Proxy 代理对象后,竟然没有出现拦截 get 操作的这个迷惑行为。

往期精彩文章