[VUE3.X]使用的Proxy和Reflect详解

328 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Proxy 代理对象

如果想要监视某个对象中属性的读写,可以使用 ECMAScript2015 之前的 Object.defineProperty() 方法为对象添加属性,这样的话就可以监测到对象属性的读写过程。这种方法应用的非常广泛,在 Vue3.0 之前的版本就是使用这样的方法来实现数据响应,从而完成双向数据绑定。

在 ECMAScript2015 当中设计了 Proxy 类型,它就是专门为对象设置访问代理器的。如果你不能理解什么是代理的话,你可以把它想象成门卫。也就是说,无论你是从里面拿东西还是放东西,都需要经过这样的一个代理。通过 Proxy 就可以轻松地监视到对象属性的读写过程。相对于Object.defineProperty() 方法,Proxy 的功能更为强大,使用起来也更为方便。

接下来,就来看看 Proxy 的具体用法。首先,通过字面量方式来定义一个 person 对象。如下代码所示:

const person = {
  name: '前端课湛',
  age: 20
}

然后,通过 new Proxy() 方式为 person 对象创建一个代理对象。如下代码所示:

const personProxy = new Proxy(person, {
  get() {},
  set() {},
  deletePropty(){},
})

在上述代码中,Proxy 构造函数中接收了两个参数,第一个参数就是代理的目标对象,第二个参数也是一个对象,可以称之为代理的处理对象。

这个代理的处理对象中的 get() 方法可以用来监视属性的访问,set() 方法可以用来监视属性的设置。

接下来,先看 get() 方法,这个方法可以接收 target 和 property 两个参数,这个方法的返回值是作为外部访问这个属性所得到的结果。如下代码所示:

const personProxy = new Proxy(person, {
  get(target, property) {
    console.log(target, property)
    return 100
  },
  set() {}
})

console.log(personProxy.name)

上述代码的运行结果如下:

{ name: '前端课湛', age: 20 } name 100

从打印的结果可以看到,这时的 get() 方法已经监听到了属性的读取。其中 target 参数指的是代理的目标对象,property 参数指的是外部访问对象的属性名,而返回值就是访问属性之后所得到的结果。

get() 方法内部正常的逻辑应该是先来判断代理目标对象当中是否存在这样一个属性,如下代码所示:

const personProxy = new Proxy(person, {
  get(target, property) {
    return property in target ? target[property] : undefined
  },
  set() {}
})

console.log(personProxy.name)
console.log(personProxy.xxx)

如果访问的属性存在的话,则返回该属性的值;如果不存在的话,则返回 undefined 或者提供的默认值。

然后,再来看一下 set() 方法,这个方法可以接收 targtpropertyvalue 三个参数。如下代码所示:

const personProxy = new Proxy(person, {
  get() {},
  set(target, property, value) {
    console.log(target, property, value)
  }
})

personProxy.gender = true

上述代码的运行结果如下:

{ name: '前端课湛', age: 20 } gender true

从打印结果可以看到,target 参数依旧表示代理的目标对象,property 参数表示设置的属性名,而 value 参数就是设置的属性值。

set() 方法内部正常的逻辑应该是为代理的目标对象设置对应的属性。如下代码所示:

const personProxy = new Proxy(person, {
  get() {},
  set(target, property, value) {
    if (property === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError(`${value} is not an integer.`)
      }
    }
    target[property] = value
  }
})

personProxy.age = 'hehehe'

这里的 set() 方法来判断设置的属性名是否为 age,如果是的话必须是整数,否则就报错。通过 personProxy 代理对象设置 age 属性值为 hehehe。运行的结果如下:

proxy.js:53
        throw new TypeError(`${value} is not an integer.`)
        ^

TypeError: hehehe is not an integer.
    at Object.set (proxy.js:53:15)
    at Object.<anonymous> (proxy.js:60:17)
    at Module._compile (internal/modules/cjs/loader.js:1201:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1221:10)
    at Module.load (internal/modules/cjs/loader.js:1050:32)
    at Function.Module._load (internal/modules/cjs/loader.js:938:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
    at internal/main/run_main_module.js:17:4

以上就是 Proxy 的一些基本用法,以后 Proxy 会越用越多,比如 Vue3.0 开始就已经使用 Proxy 来实现内部的数据响应。

Proxy 的优势

了解了 Proxy 的基本用法过后,接下来再深入探索一下相比于 Object.defineProperty() 来讲 Proxy 到底有哪些优势。

监听对象的更多操作

首先,最明显的优势在于 Proxy 更为强大一些。这个强大在于 Object.defineProperty() 只能监视属性的读写,而 Proxy 能够监视到 Object.defineProperty() 监视不到的更多对象的操作,比如 Delete 操作、调用对象的方法等等。如下代码所示:

const person = {
  name: '前端课湛',
  age: 20
}

const personProxy = new Proxy(person, {
  deleteProperty(target, property) {
    console.log(`delete ${property}`)
    delete target[property]
  }
})

delete personProxy.age
console.log(person)

上述代码为 person 对象创建了一个代理对象,并且通过代理对象的 deleteProperty() 方法监听 person 对象属性的删除操作。

上述代码的运行结果如下:

delete age
{ name: '前端课湛' }

从运行结果可以看到,Proxy 成功地监听到 Delete 操作。

Proxy 除了可以监视到对象的 Delete 操作以外,还有很多其他操作。如下表所示:

成员方法说明备注
get(target, property, receiver)读取某个属性
set(target, property, value, receiver)写入某个属性
has(target, prop)in 操作符'属性' in object
deletePropertydelete object.属性操作符
getPropertyOf(target)获取target的原型Object.getPropertyOf()
setPropertyOf(target, prototype)设置target的原型Object.setPropertyOf()
isExtensible(target)判断target是否可扩展时触发Object.isExtensible()
preventExtensions(target)阻止target的扩展性Object.preventExtensions()
getOwnProperyDescriptor(target, prop)获取target的属性prop的描述器Object.getOwnPropertyDescriptor()
defineProperty(target, property, descriptor)修改target的属性property为descriptor值.defineProperty()
ownKeys(target)返回包含所有自身属性(继承的不算)的数组Object.keys()、Object.getOwnPropertyName()、Object.getOwnPropertySymbols()
apply(target, thisArg, argumentsList)对函数target进行调用操作,可传入thisArg可数组作为调用时的参数调用一个函数
construct(target, argumentsList, newTarget)构造函数进行new操作,即实例化通过 new 调用一个函数
更好地支持数组的监听

其次,Proxy 相比于 Object.defineProperty() 可以更好地支持数组对象的监视。以往想要通过 Object.defineProperty() 监视数组对象的操作,最常见的一种方式就是通过重写数组的操作方法,这也是 Vue 所使用的方式。大体的思路就是通过自定义的方法去覆盖数组原型对象上的原有方法,以此来劫持对应方法的调用过程。 Proxy 又是如何监视数组对象的呢?如下代码所示:

const list = []

const listProxy = new Proxy(list, {
  set(target, property, value) {
    console.log('set: ', property, value)
    target[property] = value
    return true
  }
})

listProxy.push('前端课湛')

通过 Proxy 对象的 set() 方法监听数组的 push 操作,并且将指定成员添加到代理的目标数组中,最终返回 true 表示设置成功。

上述代码的运行结果如下:

set:  0 前端课湛
set:  length 1

从打印结果可以看到,这里的 0 就是数组的索引值,而前端课湛就是索引值对应的成员。也就是说,Proxy 内部可以通过数组的 push 操作来推算出来应该所处的索引值。对于数组的其他操作都是类似的,而这些操作如果通过 Object.defineProperty() 来实现的话就会特别麻烦。

非侵入的方式监听

最后,相比于 Object.defineProperty() 来讲 Proxy 的优势就是以非侵入的方式监管了对象的读写。也就是说,一个已经定义好的对象,不需要对对象本身去做任何的操作就可以监视到它内部成员的读写。如下代码所示:

const person = {
  name: '前端课湛',
  age: 20
}

const personProxy = new Proxy(person, {
  get(target, property) {
    console.log('get', property)
    return target[property]
  },
  set(target, property, value) {
    console.log('set', property, value)
    target[property] = value
  }
})

personProxy.name = '大前端'

console.log(personProxy.name)

而 Object.defineProperty() 方式要求需要通过特定的方式单独定义对象当中那些需要被监视的属性,那对于一个已经存在的对象想要监视它的属性需要去做很多额外的操作。如下代码所示:

const person = {
  name: '前端课湛',
  age: 20
}

Object.defineProperty(person, 'name', {
  get () {
    console.log('name 被访问')
    return person._name
  },
  set (value) {
    console.log('name 被设置')
    person._name = value
  }
})
Object.defineProperty(person, 'age', {
  get () {
    console.log('age 被访问')
    return person._age
  },
  set (value) {
    console.log('age 被设置')
    person._age = value
  }
})

person.name = '大前端'

console.log(person.name)

Reflect 反射对象

Reflect 是 ECMAScript2015 中提供的全新的内置对象,如果按照 Java 或者 C# 的说法,Reflect 属于一个静态类。也就是说,它不能通过 new Reflect() 构建一个实例对象,只能调用这个静态类提供的静态方法,比如 get() 等等。关于这一点应该不会太陌生,因为 JavaScript 语言中的 Math 也是这样的。

Reflect 内部封装了一系列对对象的底层操作,具体一共提供了 14 个静态方法,其中有一个被废弃了,所以目前还有 13 个。这 13 个方法的方法名与 Proxy 对象当中的处理对象的方法成员是完全一致的,其实 Reflect 的成员方法就是 Proxy 处理对象的那些方法的默认实现。如下代码所示:

const obj = {
  foo: '123',
  bar: '456'
}

const proxy = new Proxy(obj, {
  get(target, property) {
    console.log('watch logic~')

    return Reflect.get(target, property)
  }
})

console.log(proxy.foo)

当 Proxy 的处理对象当中并没有添加任何的成员时,Proxy 内部默认实现的逻辑就是调用了 Reflect 对象当中所对应的静态方法。这和上述代码当中直接将 Proxy 处理对象的 get() 方法直接调用 Reflect 对象的 get() 静态方法是一致的。

Reflect 对象的用法其实很简单,但是大多数人接触到这个对象的感觉就是为什么要有 Reflect 这样的一个对象?也就是说,Reflect 对象的价值到底体现在什么地方?Reflect 对象最大的意义就在于它提供了一套统一的用于操作对象的 API。

在这儿之前如果操作对象的话,有可能会使用 Object 对象上的一些方法,也有可能会使用比如 in 或者 delete 运算符,这些操作对新手来说实在是太乱了,并没有什么规律。如下代码所示:

const obj = {
  name: '前端课湛',
  age: 20
}

console.log('name' in obj)
console.log(delete obj['age'])
console.log(Object.keys(obj))

而 Reflect 对象就很好地解决了这样的一个问题,它统一了操作对象的方式。如下代码所示:

const obj = {
  name: '前端课湛',
  age: 20
}

console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

需要注意的是,目前的情况是之前的用法还是可以使用的,但是 ECMAScript 官方希望经过一段时间的过渡过后,以后的标准当中就会把之前的方法废弃掉。