04 Proxy 和 Reflect

ES6 新增 Proxy 和 Reflect,两者相辅相成,功能颇为强大,但工作中基本未被提及,这里略微学习一下,不求甚解,待到 coding 时遇到再温故知新。

Proxy

Proxy 概述

Proxy 用于修改某些操作的默认行为,属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写。

let obj = {}
let proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log('get--> target:', target, 'key:', key, 'receiver:', receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set--> target:', target, 'key:', key, 'value:', value)
return Reflect.set(target, key, value, receiver)
}
})
proxy.count = 1
// set--> target: {} key: count value: 1
++proxy.count
// get--> target: {count: 1} key: count receiver: Proxy {count: 1}
// set--> target: {count: 1} key: count value: 2
obj // {count: 2}

上面的代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。

ES6 原生提供 Proxy 构造函数,用于生成 Proxy 实例:

let proxy = new Proxy(target, handler)

Proxy 对象的所有用法基本一致,不同的只是 handler 参数的写法。其中,new Proxy() 表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。

如果 handler 没有设置任何拦截,那就等同于直接通向原对象。

Proxy 实例也可以作为其他对象的原型对象。

let proxy = new Proxy(
{},
{
get(target, key, receiver) {
return 42
}
}
)
let obj = Object.create(proxy)
obj.a // 42

Proxy 实例方法

下面是 Proxy 支持的所有拦截操作:

  • get(target, propKey, receiver):拦截对象属性的读取,最后一个参数 receiver 是一个可选对象。

  • set(target, propKey, value, receiver):拦截对象属性的设置,返回一个布尔值。

  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。

  • deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。

  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols (proxy)、Object.keys(proxy),返回一个数组。该方法返回目标对象所有自身属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。

  • getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, prop-Key),返回属性的描述对象。

  • defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.define Properties(proxy, propDescs),返回一个布尔值。

  • preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。

  • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。

  • isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。

  • setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

  • apply(target, object, args):拦截 Proxy 实例,并将其作为函数调用的操作。

  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 newproxy(...args)。

具体方法介绍这里不在累述,详见《ES6 标准入门》。

Proxy.revocable()

Proxy.revocable 方法返回一个可取消的 Proxy 实例。

let target = {}
let handler = {}
let { proxy, revoke } = Proxy.revocable(target, handler)
proxy.foo = 123
proxy.foo // 123
revoke()
proxy.foo // TypeError: Revoked

Proxy.revocable 方法返回一个对象,其 proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy 实例。上面的代码中,当执行 revoke 函数后再访问 Proxy 实例,就会抛出一个错误。

Proxy.revocable 的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理

const target = {
m: function() {
console.log(this === proxy)
}
}
const handler = {}
const proxy = new Proxy(target, handler)
target.m() // false
proxy.m() // true

上面的代码中,一旦 proxy 代理 target.m,后者内部的 this 就指向 proxy,而不是 target。

此外,有些原生对象的内部属性只有通过正确的 this 才能获取,所以 Proxy 也无法代理这些原生对象的属性。

const target = new Date()
const handler = {}
const proxy = new Proxy(target, handler)
proxy.getDate() // TypeError: this is not a Date object.

通过 Proxy 可以拦截网络请求或者实现数据库的 ORM 层。

Reflect

Proxy 概述

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新的 API,Reflect 对象的设计目的:

  1. 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty)放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只在 Reflect 对象上部署。

  2. 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时会抛出一个错误,而 Reflect.defineProperty(obj, name,desc) 则会返回 false。

  3. 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name) 和 Reflect.deleteProperty (obj, name) 让它们变成了函数行为。

  4. Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就使 Proxy 对象可以方便地调用对应的 Reflect 方法来完成默认行为,作为修改行为的基础。

每一个 Proxy 对象的拦截操作内部都应调用对应的 Reflect 方法,保证原生行为能够正常执行。

Reflect 对象一共有 13 个静态方法,与 Proxy 一一对应,这里不再累述,详见《ES6 标准入门》。

观察者模式

观察者模式(Observer mode)指的是函数自动观察数据对象的模式,一旦对象有变化,函数就会自动执行。

const quenedObserves = new Set()
const observe = fn => quenedObserves.add(fn)
const observable = obj => new Proxy(obj, { set })
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
quenedObserves.forEach(observe => observe())
return result
}
const person = observable({
name: '张三'
})
observe(() => console.log('name:', person.name))
person.name = '李四'

上面的代码先定义了一个 Set 集合,所有观察者函数都放进这个集合中。然后,observable 函数返回原始对象的代理,拦截赋值操作。拦截函数 set 会自动执行所有观察者。