# proxy 和 defineProperty
目前开发 Web 应用的主流框架是 React 和 Vue,这两个框架都能通过一定手段实现响应式编程,比如 Vue 本身就实现了双向绑定以及 React + Mobx 实现类似于 Vue 的操作,这个时候就是 Object.defineProperty
登场的时候。
但是随着 Vue3.0 以及 Mobx5 的推出,Proxy 取代了 Object.defineProperty
成为双向绑定的底层原理。
我们先以 Object.defineProperty
作为引入,之后讲解 Proxy,最后比较二者之间的优劣。
# Object.defineProperty
数据劫持
Object.defineProperty
方法会直接在对象上定义一个新的属性,或者修改一个对象的现有属性,并返回次对象。
该方法接受三个参数,第一个是要定义属性的对象,第二个是要定义或修改属性的名称,第三个参数是要定义或修改的属性描述符。
const obj = {}
Object.defineProperty(obj, 'prop', {
value: 18
})
console.log(obj.prop) // 18
函数的第三个参数属性描述符有两种形式:数据描述符和存取描述符。
数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 和 setter 所描述的属性。这两种描述符是互斥的。
这两种同时拥有下列两种键值:
- configurable:当且仅当该属性为 true 时,该属性的描述符才能被改变,同时该属性也能从对应的对象上被删除,默认为 false;
- enumerable:当且仅当该属性为 true 时,该属性才会出现在对象的枚举属性中,默认为 false。
数据描述符还具有以下的可选键值:
- value:该属性对应的值,可以是任意有效的 JS 值,默认为 undefined;
- writable:当且仅当该属性为 true 时,当前属性对应的值(也就是上面的 value)才能被赋值运算符改变,默认为 false。
存取描述符还具有以下的可选键值:
- get:属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值被用作属性的值;
- set:属性的 setter 函数,当属性值被修改时会调用此函数。该方法会接收一个参数(被赋予的新值),会传入赋值时的 this 对象。
# Proxy 数据拦截
Object.defineProperty
只能对对象中现有的键进行拦截,对于动态新增的键是无能为力的。同时 Object.defineProperty
只能重定义获取和设置行为。
而 Proxy 相当于一个升级,它可以进行更多的重定义。
# 概念
首先 Proxy 是一个构造函数,可以通过 new 来创建它的实例。它接受两个参数,第一个是被拦截的目标对象,第二个是 handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理实例的行为。
const p = new Proxy(target, handler)
# handler 对象的属性
handler 中的所有属性都是可选的,如果没有定义,那就会保留原对象的默认行为。
# get
对象的 getter 函数,用于拦截对象的读取操作。
# set
对象的 setter 函数,用于拦截设置属性值的操作行为。
# apply
用于拦截函数的调用。当需要被代理拦截的对象是一个函数的时候,可以通过设置 apply 来进行拦截。
# has
用于拦截 in 操作符的代理。当对目标对象使用 in 操作符时,会触发这个函数的执行。
# construct
用于拦截 new 操作符。为了使 new 操作符在生成的 Proxy 对象上生效,用于初始化代理的目标对象本身必须具有 [[Construct]]
内部方法(即 new Target
必须是有效的)。
# delete
用于拦截 delete 操作符。用于拦截对对象属性进行 delete 操作。
# defineProperty
用于拦截对象属性上的 Object.defineProperty
。当对对象进行键代理时,会触发这个方法。
# getOwnPropertyDescriptor
用于拦截 Object.getOwnPropertyDescriptor
。
# getPrototypeOf
当访问对象的原型时,会触发这个方法。
触发这个方法的条件有五个:
Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
instanceof
# isExtensible
用于拦截对象的 Object.isExtensible()
# ownKeys
用于拦截对象自身属性的读取操作。
具体拦截如下:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in 循环
该方法有几个约束条件:
- ownKeys 结果必须是一个数组
- 数组的元素类型要么是字符串要么是 Symbol
- 结果列表必须包含目标对象的所有不可配置(non-configurable)、自由属性的 key
- 如果目标对象不可扩展,那么结果列表必须包含目标对象的所有自由属性的 key,不能有其他值
# preventExtensions
用于设置对 Object.perventExtensions
的拦截。
# setPrototypeOf
用来拦截 Object.setPrototypeOf
。
# Proxy 和 Object.defineProperty
的区别
这两个属性本身就不是在同一个领域工作的,我们通常说的区别,也仅仅是针对使用了这两个 API 的 Vue 的双向绑定机制的实现。
因此,在回答的时候,通常可以直接说出 Vue 来使用这两种机制来实现双向绑定的优劣势。
# Object.defineProperty
版本
const obj = {}
Object.defineProperty(obj, 'prop', {
get() {
console.log('get val')
},
set(newVal) {
console.log(newVal)
document.getElementById('input').value = newVal
}
})
const input = document.getElementById(input)
input.addEventListener('keyup', function(e) {
obj.text = e.target.value
})
可以看出这个简单版本透露出来一个很明显的缺点,那就是只能对对象的属性进行监听,如果需要监听多个属性,则需要进行遍历。同时,这个 API 无法监听数组。
当然他的优点就是兼容性好。
# Proxy 版
const input = document.getElementById(input)
const obj = {}
const newobj = new Proxy(obj, {
get(target, key, receiver) {
console.log(`getting ${key}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(target, key, value, receiver)
if(key === 'text') {
input.value = value
}
return Reflect.set(target, key, value, receiver)
}
})
input.addEventListener('keyup', function(e) {
obj.text = e.target.value
})
从上述可以看到,Proxy 可以对整个对象进行代理拦截,并且返回一个新的对象。除此之外还可以对数组进行拦截。
Proxy 最大的问题便是兼容性不好,并且无法通过 polyfill 磨平。