代理与反射

  • 代理基础
  • 代码捕获器与反射方法
  • 代理模式

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。 可以给目标对象定义一个关联的代理对象,这个代理对象可以作为抽象的目标对象来使用。 在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

由于代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转换为之前的 ECMAScript 代码,因为代理的行为实际上是无可替代的。为此,代理和反射只能在百分百支持它们的平台使用。

代理基础

代理是目标对象的抽象,可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。

代理类似 C++ 指针

创建空代理

最简单的代理是空代理。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。

代理是使用 Proxy 构造函数创建的。

Proxy 构造函数接受两个参数:目标对象和处理程序对象。缺少任何一个参数都会抛出 TypeError。

const target = {
  id: 'target'
}

const handler = {}

const proxy = new Proxy(target, handler)

target.id // 'target'
proxy.id  // 'target'

target.id = 'foo'
target.id // 'foo'
proxy.id  // 'foo'

proxy.id = 'bar'
target.id // 'bar'
proxy.id  // 'bar'

target.hasOwnProperty('id') // true
proxy.hasOwnProperty('id')  // true

// Proxy.prototype 是 undefined, 因此不能用 instanceof 操作符
target instanceof Proxy // TypeError: Function has non-object prototype 'undefined' in instanceof check
proxy instanceof Proxy  // TypeError: Function has non-object prototype 'undefined' in instanceof check

// 严格相等可以用来区分代理和目标
target === proxy // false

定义捕获器

使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的"基本操作的拦截器"。

捕获器(trap)是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。

可以定义一个 get() 捕获器,在 ECMAScript 操作以某种形式调用 get() 时触发。 当通过代理对象执行 get() 操作时,就会触发定义的 get() 捕获器。 get() 不是 ECMAScript 对象可以调用的方法。 proxy[property]、proxy.property、Object.createproxy 等操作都能触发基本的 get() 操作以获取属性。

只有在代理对象上执行这些操作才会触发捕获器。目标对象上仍会产生正常的行为。

const target = {
  foo: 'bar'
}

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

const proxy = new Proxy(target, handler)

target.foo // 'bar'
proxy.foo  // 'hander override'

Object.create(target)['foo'] // 'bar'
Object.create(proxy)['foo'] // 'hander override'

捕获器参数和反射 API

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

get() 捕获器会接受到三个参数:目标对象、要查询的属性、代理对象

const target = {
  foo: 'bar'
}

const handler = {
  get(trapTarget, property, receiver) {
    console.log(trapTarget === target)
    console.log(property)
    console.log(receiver === proxy)
  }
}

const proxy = new Proxy(target, handler)

proxy.foo
// true
// foo
// true

根据这些参数,可以重建被捕获方法的原始行为

const target = {
  foo: 'bar'
}
const handler = {
  get(trapTarget, property, receiver) {
    return trapTarget[property]
  }
}
const proxy = new Proxy(target, handler)
console.log(proxy.foo)   // bar
console.log(target.foo)  // bar

实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局 Reflect 对象上的同名方法来轻松重建。

处理程序对象中所有可能捕获的方法都有对应的反射 API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名。

const target = {
  foo: 'bar'
}

const handler = {
  get() {
    return Reflect.get(...arguments)
  }
}

// 简写
const handler2 = {
  get: Reflect.get
}

const proxy = new Proxy(target, handler)
const proxy2 = new Proxy(target, handler2)

proxy.foo  // bar
target.foo // bar

反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法。

const target = {
   foo: 'bar',
   baz: 'qux'
}

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

const proxy = new Proxy(target, handler)

console.log(proxy.foo)   // bar!!!
console.log(target.foo)  // bar
console.log(proxy.baz)   // qux
console.log(target.baz)  // qux

捕获器不变式

根据 ECMAScript 规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循"捕获器不变式"。

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

可撤销代理

有时候可能需要中断代理对象和目标对象之间的联系。

revocable() 方法支持撤销代理对象和目标对象的关联。撤销代理的操作是不可逆的。撤销函数是幂等的,调用多少次都一样。撤销代理之后再调用代理会抛出 TypeError。

实用反射 API

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

  1. 反射 API 与对象 API
  • 反射 API 并不限于捕获处理程序
  • 大多数反射 API 方法在 Object 类型上有对应的方法
  1. 状态标记

很多反射方法返回称作"状态标记"的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误的反射 API 方法更有用。

const o = {}
try {
  Object.defineProperty(o, 'foo', 'bar')
  console.log('success')
} catch(e) {
  console.log('failure')
}

// 与上述操作等同
if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
  console.log('success')
} else {
  console.log('failure')
}

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

  • Reflect.defineProperty()
  • Reflect.preventExtensions()
  • Reflect.setPrototypeOf()
  • Reflect.set()
  • Reflect.deleteProperty()
  1. 用一等函数替代操作符
  • Reflect.get(): 替代对象属性访问操作符
  • Reflect.set(): 替代 = 操作符
  • Reflect.has(): 替代 in 操作符
  • Reflect.deleteProperty(): 替代 delete 操作符
  • Reflect.construct(): 替代 new 操作符
  1. 安全地应用函数

通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 函数。

可以直接使用定义在 Function 原型上的 apply 方法。

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

// 上述代码完全可以使用 Reflect.apply 来避免
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

使用 Proxy 创建代理实例,this 指向的是 代理对象。

解决方法:使用 Proxy 和 target,创建代理类 ClassProxy,再创建并初始化代理的实例。

  1. 代理与内部槽位

有些 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: 'this' is not a Date object

代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作。

get()

在获取属性值的操作中被调用,对应的反射方法为 Reflect.get()

  1. 返回值无限制。

  2. 拦截的操作:

  • proxy.property
  • proxy[property]
  • Object.createproxy
  • Reflect.get(proxy, property, receiver)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用的目标对象上的字符串键属性
  • receiver: 代理对象或继承代理对象的对象
  1. 捕获器不变式

如果 target.property 不可写且不可配置,则处理程序返回的值 必须与 target.property 匹配。

如果 target.property 不可配置且[[Get]]特性为 undefined,处理程序的返回值也必须是 undefined。

set()

在设置属性值的操作中调用。对应的反射方法为 Reflect.set()

  1. 返回值

true 表示成功;false 表示失败,严格模式下会抛出 TypeError

  1. 拦截的操作
  • proxy.property = value
  • proxy[property] = value
  • Object.createproxy = value
  • Reflect.set(proxy, property, value, receiver)
  1. 捕获器处理程序参数
  • target:
  • property:
  • value:
  • receiver:
  1. 捕获器不变式

如果 target.property 不可写且不可配置,则不能修改目标属性的值。

如果 target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性的值。

在严格模式下,处理程序中返回 false 会抛出 TypeError。

has()

在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。

  1. 返回值,布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。

  2. 拦截的操作

  • property in proxy
  • property in Object.create(proxy)
  • with(proxy) {(property)}
  • Reflect.has(proxy, property)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用的目标对象上的字符串键属性
  1. 捕获器不变式

如果 target.property 存在且不可配置,则处理程序必须返回 true。 如果 target.property 存在且目标对象不可扩展,则处理程序必须返回 true。

defineProperty()

在 Object.defineProperty() 中被调用。对应的反射 API 方法为 Reflect.defineProperty().

  1. 返回值,布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。

  2. 拦截的操作

  • Object.defineProperty(proxy, property, descriptor)
  • Reflect.defineProperty(proxy, property, descriptor)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用的目标对象上的字符串键属性
  • descriptor: 包含可选的 enumerable、configurable、writable、value、get、set 定义的对象
  1. 捕获器不变式

如果目标对象不可扩展,则无法定义属性。

如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性。

如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性。

getOwnPropertyDescriptor()

在 Object.getOwnPropertyDescriptor() 中被调用。对应的反射 API 为 Reflect.getOwnPropertyDescriptor().

  1. 返回值,必须是对象,或者属性不存在时返回 undefined。

  2. 拦截的操作

  • Object.getOwnPropertyDescriptor(proxy, property)
  • Reflect.getOwnPropertyDescriptor(proxy, property)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用的目标对象上的字符串键属性
  1. 捕获器不变式

如果自有的 target.property 存在且不可配置,则处理程序必须返回一个表示该属性存在的对象。

如果自有的 target.property 存在且可配置,则处理程序必须返回表示该属性可配置的对象。

如果自有的 target.property 存在且 target 不可扩展,则处理程序必须返回一个表示该属性存在的对象。

如果自有的 target.property 不存在且 target 不可扩展,则处理程序必须返回 undefined 表示该属性不存在。

如果自有的 target.property 不存在,则处理程序不能返回表示该属性可配置的对象。

deleteProperty()

在 delete 操作符中被调用。对应的反射 API 方法为 Reflect.deleteProperty()。

  1. 返回值,必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。

  2. 拦截的操作

  • delete proxy.property
  • delete proxy[property]
  • Reflect.deleteProperty(proxy, property)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用的目标对象上的字符串键属性
  1. 捕获器不变式

如果自有的 target.property 存在且不可配置,则处理程序不能删除这个属性。

ownKeys()

在 Object.keys() 及类似方法中被调用。对应的反射 API 方法为 Reflect.ownKeys()。

  1. 返回值,必须返回包含字符串或符号的可枚举对象。

  2. 拦截的操作

  • Object.getOwnPropertyNames(proxy)
  • Object.getOwnPropertySymbols(proxy)
  • Object.keys(proxy)
  • Reflect.ownKeys(proxy)
  1. 捕获器处理程序参数
  • target: 目标对象
  1. 捕获器不变式

返回的可枚举对象必须包含 target 的所有不可配置的自有属性。

如果 target 不可扩展,则返回可枚举对象必须准确地包含自有属性键。

getPrototypeOf()

在 Object.getPrototypeOf() 中被调用。对应的反射 API 方法为 Reflect.getPrototypeOf().

  1. 返回值,必须是对象,或者 null。

  2. 拦截的操作

  • Object.getPrototypeOf(proxy)
  • Reflect.getPrototypeOf(proxy)
  • proxy.__proto__
  • Object.prototype.isPrototypeOf(proxy)
  • proxy instanceof Object
  1. 捕获器处理程序参数
  • target: 目标对象
  1. 捕获器不变式

如果 target 不可扩展,则 Object.getPrototypeOf(proxy) 唯一有效的返回值就是 Object.getPrototypeOf(target) 的返回值。

setPrototypeOf()

在 object.setPrototypeOf() 中被调用。对应的反射 API 方法为 Reflect.setPrototypeOf()。

  1. 返回值,必须返回布尔值,表示原型赋值是否成功。返回非布尔值会被转型为布尔值。

  2. 拦截的操作

  • Object.setPrototypeOf(proxy)
  • Reflect.setPrototypeOf(proxy)
  1. 捕获器处理程序参数
  • target: 目标对象
  • prototype: target 的替代原型,如果是顶级原型则为 null。
  1. 捕获器不变式

如果 target 不可扩展,则唯一有效的 prototype 参数就是 Object.getPrototypeOf(target) 的返回值。

isExtensible()

在 Object.isExtensible() 中被调用。对应的反射 API 方法为 Reflect.isExtensible()。

  1. 返回值,必须返回布尔值,表示 target 是否可扩展。返回非布尔值会被转型为布尔值。

  2. 拦截的操作

  • Object.isExtensible(proxy)
  • Reflect.isExtensible(proxy)
  1. 捕获器处理程序参数
  • target: 目标对象
  1. 捕获器不变式

如果 target 可扩展,则处理程序必须返回 true。

如果 target 不可扩展,则处理程序必须返回 false。

preventExtensions()

在 Object.preventExtensions() 中被调用。对应的反射 API 方法为 Reflect.preventExtensions()

  1. 返回值,必须返回布尔值,表示 target 是否已经不可扩展。返回非布尔值会被转型为布尔值。

  2. 拦截的操作

  • Object.preventExtensions(proxy)
  • Reflect.preventExtensions(proxy)
  1. 捕获器处理程序参数
  • target: 目标对象
  1. 捕获器不变式

如果 Object.isExtensible(proxy) 是 false,则处理程序必须返回 true。

apply()

在调用函数时被调用。对应的反射 API 方法为 Reflect.apply()。

  1. 返回值,无限制。

  2. 拦截的操作

  • proxy(...argumentsList)
  • Function.prototype.apply(thisArg, arguments)
  • Function.prototype.call(thisArg, ...arguments)
  • Reflect.apply(target, thisArgument, argumentsList)
  1. 捕获器处理程序参数
  • target: 目标对象
  • thisArg: 调用函数时的 this 参数
  • argumentsList: 调用函数时的参数列表
  1. 捕获器不变式

target 必须是一个函数对象。

construct()

在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()。

  1. 返回值,必须返回一个对象。

  2. 拦截的操作

  • new proxy(...argumentsList)
  • Reflect.construct(target, argumentsList, newTarget)
  1. 捕获器处理程序参数
  • target: 目标对象
  • argumentsList: 传给目标构造函数的参数列表
  • newTarget: 最初被调用的构造函数
  1. 捕获器不变式

target 必须可以用作构造函数。

代理模式

跟踪属性访问

通过捕获 get、set、has 等操作,可以知道对象属性什么时候被访问、被查询。

把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过。

const user = {
  name: 'Jake'
}

const proxy = new Proxy(user, {
  get(target, property, receiver) {
    console.log(`Getting ${property}`)
    return Reflect.get(...arguments)
  },
  set(target, property, value, receiver) {
    console.log(`Setting ${property}=${value}`)
    return Reflect.set(...arguments)
  }
})

proxy.name     // Getting name
proxy.age = 27 // Setting age=27

隐藏属性

代理的内部实现对外部代码是不可见的,所以可以通过某些设置,隐藏目标对象上的属性。

属性验证

所有的赋值操作都会触发 set() 捕获器,可以根据赋值决定时允许还是拒绝。

函数与构造函数参数验证

可以使用 apply() 对参数进行条件限定;也可以使用 construct() 对实例化时进行参数限定

数据绑定与可观察对象

通过代理可以把运行时中原本不相关的部分联系到一起。

  • 可以讲被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中
const userList = []
class User {
  constructor(name) {
    this.name_ = name
  }
}
const proxy = new Proxy(User, {
  construct() {
    const newUser = Reflect.construct(...arguments)
    userList.push(newUser)
    return newUser
  }
})

new proxy('John')
new proxy('Jacob')
new proxy('Jingleheimerschmidt')

console.log(userList) // [User {}, User {}, User{}]
  • 可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送信息
const userList = []

function emit(newValue) {
  console.log(newValue)
}

const proxy = new Proxy(userList, {
  set(target, property, value, receiver) {
    const result = Reflect.set(...arguments)
    if (result) {
      emit(Reflect.get(target, property, receiver))
    }
    return result
  }
})
proxy.push('John')
// John
proxy.push('Jacob')
// Jacob