关注前端小讴,阅读更多原创技术文章

代理基础

  • ES6 为的代理和反射为开发者提供拦截并向基本操作嵌入额外行为的能力
  • 代理是目标对象的抽象,其可以用作目标对象的替身,但完全独立于目标对象
  • 目标对象既可直接被操作,也可通过代理来操作,直接操作会绕过代理施予的行为

相关代码 →

创建空代理

  • 使用Proxy构造函数创建代理,接收目标对象处理程序对象两个参数(缺一不可)
  • 空代理是最简单的代理,可用空对象作为处理程序对象,空代理对象仅作为一个抽象的目标对象
const target = {
  // 目标对象
  id: 'target',
}
const handler = {} // 处理程序对象(空对象)
const proxy = new Proxy(target, handler) // 创建空代理
  • (默认情况下)空代理对象上执行的所有操作都会应用到目标对象,反之亦然
console.log(target.id) // 'target'
console.log(proxy.id) // 'target'

target.id = 'foo' // 目标对象属性重新赋值
console.log(target.id) // 'foo'
console.log(proxy.id) // 'foo',会反映到代理上

proxy.id = 'bar' // 代理属性重新赋值
console.log(target.id) // 'bar',会反映到目标对象上
console.log(proxy.id) // 'bar'

console.log(target.hasOwnProperty('id')) // true
console.log(proxy.hasOwnProperty('id')) // true
  • Proxy构造函数没有prototype属性,也不能使用instanceof操作符检测
console.log(Proxy) // [Function: Proxy]
console.log(Proxy.prototype) // undefined
console.log(proxy instanceof Proxy) // TypeError: Function has non-object prototype 'undefined' in instanceof check
  • 使用严格相等===用以区分代理和目标
console.log(target === proxy) // false

定义捕获器

  • 使用代理的主要目的是可以定义捕获器,即基本操作的拦截器
  • 每个捕获器对应一种基本操作,可以直接或间接在代理上调用,调用操作时会先调用捕获器函数,再将操作传播到目标对象
const target2 = {
  foo: 'bar',
}
const handler2 = {
  // 定义get()捕获器函数,以方法名为键
  get() {
    return 'handler override'
  },
}
const proxy2 = new Proxy(target2, handler2)
  • get()函数可以通过多种形式触发并被捕获器拦截到:proxy[property]proxy.propertyObject.create(proxy)[property]
  • 只有代理对象上执行操作才会触发捕获器,目标对象上不会
console.log(proxy2.foo) // 'handler override',代理对象上操作
console.log(proxy2['foo']) // 'handler override',代理对象上操作
console.log(Object.create(proxy2).foo) // 'handler override',代理对象上操作

console.log(target2.foo) // 'bar',目标对象上操作
console.log(target2['foo']) // 'bar',目标对象上操作
console.log(Object.create(target2).foo) // 'bar',目标对象上操作

捕获器参数和反射 API

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

const target3 = {
  foo: 'bar',
}
const handler3 = {
  // get()捕获器接收3个参数:目标对象、要查询的属性、代理对象
  get(tar, pro, rec) {
    console.log(tar === target3)
    console.log(pro)
    console.log(rec === handler3)
  },
}
const proxy3 = new Proxy(target3, handler3)
proxy3.foo
/* 
  true
  'foo'
  false
*/
  • 捕获器利用这些参数重建被捕获方法的原始行为
const handler4 = {
  get(tar, pro, rec) {
    return tar[pro] // target3['foo']
  },
}
const proxy4 = new Proxy(target3, handler4)
console.log(proxy4.foo) // 'bar'
  • 处理对象的所有捕获器方法都有对应的反射 API 方法同名行为相同),方法存在于全局对象Reflect
const handler5 = {
  get() {
    return Reflect.get(...arguments) // 用arguments解耦
  },
  // get: Reflect.get, // 更简洁的写法
}
const proxy5 = new Proxy(target3, handler5)
console.log(proxy5.foo) // 'bar'
  • 创建一个可以捕获所有方法,并将每个方法都转发给反射 API 的空代理,可不定义处理程序对象
const proxy6 = new Proxy(target3, Reflect)
console.log(proxy6.foo) // 'bar'
  • 利用反射 API,可用最少的代码修改捕获的方法
const target4 = {
  foo: 'bar',
  baz: 'qux',
}
const handler6 = {
  get(tar, pro, rec) {
    let dec = ''
    pro === 'foo' && (dec = '!!!')
    return Reflect.get(...arguments) + dec
  },
}
const proxy7 = new Proxy(target4, handler6)
console.log(proxy7.foo) // 'bar!!!'
console.log(proxy7.baz) // 'qux'

捕获器不变式

  • 捕获处理程序的行为必须遵循捕获器不变式
  • 如:目标对象有一个不可配置不可重写的属性,捕获器修改返回值会报错
const target5 = {}
Object.defineProperty(target5, 'foo', {
  configurable: false, // 不可配置
  writable: false, // 不可重写
  value: 'bar',
})
const handler7 = {
  get() {
    return 'qux'
  },
}
const proxy8 = new Proxy(target5, handler7)
console.log(proxy8.foo) // TypeError: 'get' on proxy: property 'foo' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'bar' but got 'qux')

可撤销代理

  • new Proxy()创建的代理不可撤销,会在代理对象生命周期内一直存在
  • Proxy.revocable()方法可以用来创建一个可撤销的代理对象

    • 接收目标对象处理对象2 个参数
    • 返回结构为{"proxy":proxyObj,"revoke":revokeFun}的对象
    • proxy为代理对象;revoke为撤销方法,调用时不需参数
    • 撤销函数revoke()幂等,调用多次结果相同
    • 撤销操作不可逆,撤销后再次调用代理会报错
const target6 = {
  foo: 'bar',
}
const handler8 = {
  get() {
    return 'intercepted'
  },
}
const revocable = Proxy.revocable(target6, handler8)
const proxy9 = revocable.proxy // 创建可撤销代理
console.log(proxy9.foo) // 'intercepted'

revocable.revoke() // 撤销代理
revocable.revoke() // 撤销代理,调用多次结果相同
revocable.revoke() // 撤销代理,调用多次结果相同
// console.log(proxy9.foo) // TypeError: Cannot perform 'get' on a proxy that has been revoked

实用反射 API

反射 API 与对象 API

  • 反射 API 不限于捕获处理程序
  • 大多数反射 API 在 Object 类型上有对应的方法:

    • Object上的方法适用于通用程序,反射方法适用于细粒度的对象控制与操作

状态标记

  • 以下反射方法提供状态标记,返回布尔值表示操作是否成功

    • Reflect.defineProperty()Reflect.preventExtensions()Reflect.setPrototypeOf()Reflect.set()Reflect.deleteProperty()
    • (参数格式正确)操作失败时,不会抛出错误,而是返回false
const o = {}
Object.defineProperty(o, 'foo', {
  writable: false, // 不可重写
})

Object.defineProperty(o, 'foo', { value: 'bar' }) // TypeError: Cannot redefine property: foo,Object.defineProperty()定义不成功会抛出错误
Reflect.defineProperty(o, 'foo', { value: 'bar' }) // Reflect.defineProperty()定义不成功不会抛出错误
console.log(Reflect.defineProperty(o, 'foo', { value: 'bar' })) // false,Reflect.defineProperty()返回“状态标记”的布尔值

// 重构后的代码
if (Reflect.defineProperty(o, 'foo', { value: 'bar' })) {
  console.log('success')
} else {
  console.log('failure') // 'failure'
}

用一等函数替代操作符

  • 以下反射方法提供只有通过操作符才能完成的操作

    • Reflect.get():可以替代对象属性访问操作符
    • Reflect.set():可以替代赋值操作符=
    • Reflect.has():可以替代in操作符或with()
    • Reflect.deleteProperty():可以替代delete操作符
    • Reflect.construct():可以替代new操作符
const o2 = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar
  },
}
Reflect.get(o2, 'foo') // 1
Reflect.set(o2, 'foo', 3)
console.log(o2.foo) // 3
Reflect.has(o2, 'foo') // true
Reflect.deleteProperty(o2, 'bar')
console.log(o2.bar) // undefined
const arr = Reflect.construct(Array, [1, 2, 3])
console.log(arr) // [ 1, 2, 3 ]

安全地应用函数

  • 对函数原型对象Function.prototypeapply方法利用call进行绑定时,Reflect.apply()可以使代码更加简洁易懂
const f1 = function () {
  console.log(arguments[0] + this.mark)
}
const o3 = {
  mark: 95,
}
f1.apply(o3, [15]) // 110,将f1的this绑定到o3
Function.prototype.apply.call(f1, o3, [15]) // 110,函数的原型对象的apply方法,利用call进行绑定
Reflect.apply(f1, o3, [15]) // 110,通过指定的参数列表发起对目标函数的调用,三个参数(目标函数、绑定的this对象、实参列表)

有关 Reflect 对象的详细文档 →

代理另一个代理

  • 创建一个代理,通过它代理另一个代理,从而在一个目标对象上构建多层拦截网
const target7 = {
  foo: 'bar',
}
const firstProxy = new Proxy(target7, {
  // 第一层代理
  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'
*/

代理的问题与不足

代理中的 this

  • 代理中的this值是潜在的问题来源,例如方法中的this通常指向调用该方法的对象
const target8 = {
  thisValEqualProxy() {
    return this === proxy10
    /* 
      this指向:
      在实例中,指向实例本身
      在代理中,指向代理对象
    */
  },
}
const proxy10 = new Proxy(target8, {})
console.log(target8.thisValEqualProxy()) // false
console.log(proxy10.thisValEqualProxy()) // true
  • 目标对象依赖于对象标识时,this的指向会产生问题
const wm = new WeakMap()
class User {
  constructor(userId) {
    wm.set(this, userId) // 使用目标对象作为WeakMap的键
    /* 
      this的指向:目标对象
    */
  }
  get id() {
    return wm.get(this)
    /* 
      this的指向:
      在实例中,指向实例本身 User {}
      在代理中,指向代理对象
    */
  }
}

const user = new User(123)
console.log(wm) // WeakMap {User => 123}
console.log(user.id) // 123

const userInstanceProxy = new Proxy(user, {}) // 代理user实例,User类constructor中的this指向User类实例
console.log(wm) // WeakMap {User => 123},弱键未发生变化
console.log(userInstanceProxy.id) // undefined
  • 代理实例改为代理类本身,再创建代理实例,解决问题
const userClassProxy = new Proxy(User, {}) // 代理User类本身
const proxyUser = new userClassProxy(456) // 创建代理实例,User类constructor中的this指向代理实例
console.log(wm) // WeakMap {User => 123, User => 456},弱键发生变化,追加了以代理作为键
console.log(proxyUser.id) // 456

代理与内部槽位

  • 有些内置类型可能会依赖代理无法控制的机制:如Date类型方法的执行依赖this值上的内部槽位[[NumberDate]],而该槽位不存在于代理对象,且无法被get()set()操作访问到
const target9 = new Date()
const proxy11 = new Proxy(target9, {})
console.log(target9.getDate()) // 24,当天日期
console.log(proxy11.getDate()) // TypeError: this is not a Date object.

总结 & 问点

  • 代理的用处是什么?其和目标对象有怎样的关系?
  • 如何创建空代理?如何区分空代理对象和目标对象?
  • 什么是捕获器?其是如何被调用和触发的?get()函数可以通过哪些形式被捕获器拦截?
  • get()捕获器接收哪些参数?写一段代码,利用这些参数重写捕获方法的原始行为
  • 如何创建可撤销代理?撤销后再次撤销会怎样?撤销后调用代理会怎样?
  • 如何理解 Reflect 对象?其反射 API 与对象 API 有怎样的关联和异同?如何理解 Reflect.apply()方法?
  • 如何通过代理,在一个目标对象上构建多层拦截网?
  • 代理有哪些潜在的问题?如何解决呢?

小讴
217 声望16 粉丝