乐趣区

关于前端:将原型对象设置成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 操作的这个蛊惑行为。

这更加激发了我钻研 Vue 3 源码的趣味了,点赞➕关注,等我钻研完源码就开始写文章!

退出移动版