代理与反射

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力

代理基础

代理是目标对象的抽象

创建空代理

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做

代理使用 Proxy 构造函数创建,接收两个必填参数:目标对象和处理程序对象

const target = {
	id: 'target',
}

const handler = {}

const proxy = new Proxy(target, handler)

console.log(target === proxy) // false

针对上述代码中 targetproxy 两个对象,注意:

  • hasOwnProperty() 方法都会应用到目标对象
  • Proxy.prototype 是 undefined,因此不能使用 instanceof 操作符检查对象是否是代理

定义捕获器

使用代理的主要目的是可以定义捕获器(trap)

例如,定义一个 get() 捕获器,在 ECMAScript 操作以某种形式调用 get() 时触发:

const target = {
	foo: 'bar',
}

const handler = {
	get() {
		return 'handler override'
	},
}

const proxy = new Proxy(target, handler)

注意:

  • get() 不是 ECMAScript 对象可以调用的方法
  • proxy[property]、proxy.property 或 Object.create(proxy)[property] 等操作都可以触发基本的 get() 操作以获取属性

捕获器参数和反射 API

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为

比如,get() 捕获器接收以下参数:

  • 目标对象
  • 要查询的属性
  • 代理本身

通过这些参数可以重建被捕获方法的原始行为:

const target = {
	foo: 'bar',
}

const handler = {
	get(trapTarget, property, receiver) {
		return trapTarget[property]
	},
}

const proxy = new Proxy(target, handler)

但并非所有捕获器行为都像 get() 这么简单,因此 ECMAScript 6 为所有捕获器定义了一组默认行为,这些行为可以在 Reflect 对象上找到

处理程序对象所有可以捕获的方法都有对应的反射(Reflect)API 方法,使用反射 API 定义空代理对象:

const target = {
	foo: 'bar',
}

const proxy = new Proxy(target, Reflect)

在反射 API 的基础上可以用最少的代码修改捕获的方法。比如,在某个属性被访问时,对返回的值进行一番修饰:

const target = {
	foo: 'bar',
}

const handler = {
	get(trapTarget, property, receiver) {
		const decoration = property === 'foo' ? '!!!' : ''
		return Reflect.get(...arguments) + decoration
	},
}

const proxy = new Proxy(target, handler)

console.log(proxy.foo) // bar!!!

捕获器不变式

捕获器处理程序的行为必须遵守“捕获器不变式(trap invariant)”

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError

可撤销代理

new Proxy() 创建的代理对象与目标对象之间的联系会在代理对象的生命周期内一直存在

Proxy.revocable() 方法创建一个可撤销的代理,返回一个对象,包含两个属性:

  • proxy:新创建的代理对象
  • revoke:一个函数,调用后会撤销代理

注意:

  • 撤销代理的操作时不可逆的
  • 撤销函数(revoke())是幂等的,调用多少次结果都一样
  • 撤销代理之后再调用代理会抛出 TypeError

实用反射 API

某些情况下应该优先使用反射 API:

1. 反射 API 与 对象 API

  • 反射 API 并不限于捕获处理程序
  • 大多数反射 API 方法在 Object 类型上有对应的方法
  • Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作

2. 状态标记

很多反射方法返回称作“状态标记”的布尔值,表示操作是否成功。

以下方法都会提供状态标记:

  • Reflect.defineProperty()
  • Reflect.preventExtensions()
  • Reflect.setPrototypeOf()
  • Reflect.set()
  • Reflect.deleteProperty()

3. 用一等函数替代操作符

  • Reflect.get(),替代对象属性访问操作符
  • Reflect.set(),替代=赋值操作符
  • Reflect.has(),替代 in 操作符或 with()
  • Reflect.deleteProperty(),替代 delete 操作符
  • Reflect.construct(),替代 new 操作符

4. 安全地应用函数

在通过 apply 方法调用函数,被调用的函数可能也定义了自己的 apply 属性,此时:

Function.prototype.apply.call(myFunc, thisVal, argumentList)

// 可替换为
Reflect.apply(myFunc, thisVal, argumentList)

代理另外一个代理

代理可以拦截反射 API 的操作,而这意味着完全可以创建一个代理,通过它去代理另外一个代理。这样就可以在一个目标对象上建立多层拦截网:

const target = {
	foo: 'bar',
}

const firstProxy = new Proxy(target, {
	get() {
		console.log('first proxy')
		return Reflect.get(...arguments)
	},
})

const secondProxy = new Proxy(firstProxy, {
	get() {
		console.log('second proxy')
		return Reflect.get(...arguments)
	},
})

console.log(secondProxy.foo)
// second proxy
// first proxy
// bar

代理的问题与不足

1. 代理中的 this

如果目标对象依赖于对象标识,那就可能遇到意料之外的问题

2. 代理与内部插槽 有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错

比如,Date 类型方法的执行依赖 this 值上的内部槽位 [[NumberDate]],代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:

const target = new Date()
const proxy = new Proxy(target, {})
console.log(proxy instanceof Date) // true
proxy.getDate() // TypeError

代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作,这些操作有不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式

  • get()
  • set()
  • has()
  • definedProperty()
  • getOwnPropertyDescriptor()
  • deleteProperty()
  • ownKeys()
  • getPrototypeOf()
  • setPrototypeOf()
  • isExtensible()
  • preventExtensions()
  • apply()
  • construct()

代理模式

使用代理可以在代码中实现一些有用的编程模式

  • 跟踪属性访问
  • 隐藏属性
  • 属性验证
  • 函数与构造函数参数验证
  • 数据绑定与可观察对象
Last Updated: