Proxy

29 阅读4分钟

ES6 新增的代理和反射可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

创建空代理

代理是使用Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。要创建空代理,可以传一个简单的对象字面量作为处理程序对象,从而让所有操作畅通无阻地抵达目标对象。

handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target

 const target = {}
 const handler = {}
 ​
 const proxy = new Proxy(target, handler)
 ​
 // Proxy.prototype 是undefined,因此不能使用instanceof 操作符
 console.log(target instanceof Proxy) // TypeError
 ​
 // 严格相等可以用来区分代理和目标
 console.log(target === proxy) // false

定义捕获器 - trap

捕获器就是在处理程序对象中定义的“基本操作的拦截器”。

只有在代理对象上执行proxy[property]、proxy.property 或Object.create(proxy)[property] 等操作都会触发基本的get 操作以获取属性。在目标对象上执行这些操作仍会产生正常的行为。

 const target = {
     foo: 'bar'
 }
 const handler = {
     get() { return 'handler override' }
 }
 const proxy = new Proxy(target, handler)
 console.log(target.foo) // bar
 console.log(proxy.foo) // handler override

捕获器参数和反射API

所有捕获器都可以访问响应的参数,基于这些参数可以重建被捕获方法的原始行为。如,get() 捕获器会接收到目标对象、要查询的属性和代理对象3 个参数。

 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

处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且具有与被拦截方法相同的行为。

 const handler = {
     get(trapTarget, property, receiver) {
         // return trapTarget[property]
         return Reflect.get(...arguments)
     }
     // 或者写的更简洁
     get: Reflect.get
 }

如果想创建一个可以捕获所有方法,然后将每个方法转发给对应反射API 的空代理,那么甚至不需要定义处理程序对象。

 const target = {
     foo: 'bar'
 }
 const proxy = new Proxy(target, Reflect)

捕获器不变式 - trap invariant

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

 const target = {}
 Object.defineProperty(target, 'foo', {
     configurable: false,
     writable: false,
     value: 'bar'
 })
 const handeler = {
     get() {
         return 'abc'
     }
 }
 const proxy = new Proxy(target, handler)
 console.log(proxy.foo) // TypeError

可撤销代理

Revocable() 方法支持撤销代理对象与目标对象的关联。撤销代理之后再调用代理会抛出TyperError。

 const target = {
     foo: 'bar'
 }
 const handler = {
     get() {
         return 'intercepted'
     }
 }
 const { proxy, revoke } = Proxy.revocable(target, handler)
 console.log(proxy.foo) // intercepted
 console.log(target.foo) // bar
 ​
 revoke()
 console.log(proxy.foo) // TypeError

代理另一个代理

代理可以拦截反射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

代理的问题

代理中的this

const wm = new WeakMap()
class User {
    constructor(userId)  {
        wm.set(this, userId)
    }
    set id(userId) {
        wm.set(this, userId)
    }
    get id() {
        return wm.get(this)
    }
}

const user = new User(123)
console.log(user.id) // 123

const userInstanceProxy = new Proxy(user, {})
console.log(userInstanceProxy.di) // undefined
// User 实例一开始使用目标对象作为WeakMap 的键,代理对象却尝试从自身取得这个实例。

要解决这个问题,就需要重新配置代理,把代理User 实例改为代理User 类本身。之后再创建代理的实例就会以代理实例作为WeakMap 的键了。

const UserClassProxy = new Proxy(User, {})
const proxyUser = new UserClassProxy(456)
console.log(proxyUser.id)

代理与内部槽位

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

根据ECMAScript 规范,Date 类型方法的执行依赖this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的get 和set 操作访问到。

const target = new Date()
const proxy = new Proxy(target, {})
console.log(proxy instanceof Date) // true
proxy.getDate() // TypeError: 'this' is not a Date object

代理捕获器与反射方法

1、get()

拦截的操作:

  • proxy.property
  • proxy[property]
  • Object.create(proxy)[property]
  • Reflect.get(proxy, property, receiver)

2、set()

拦截的操作:

  • proxy.property = value
  • proxy[property] = value
  • Object.create(proxy)[property] = value
  • Reflect.set(proxy, property, value, receiver)

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

拦截的操作:

  • property in proxy
  • property in Object.create(proxy)
  • with(proxy) {(property)}
  • Reflect.has(proxy, property)

4、defineProperty() 捕获器会在Object.defineProperty()中被调用

5、getOwnPropertyDescriptor()

6、deleteProperty()

7、ownKeys()

8、getPrototypeOf()

9、setPrototypeOf()

10、isExtensible()

11、preventExtensions()

12、apply

拦截的操作:

  • proxy(…argumentsList)
  • Function.prototype.apply(thisArg, argumentsList)
  • Function.prototype.call(thisArg, …argumentsList)
  • Reflect.apply(target, thisArgument, argumentsList)

13、construct()

拦截的操作:

  • new proxy(…argumentsList)
  • Reflect.construct(target, argumentsList, newTarget)

Proxy 实现简单的数据绑定

<body>
    hello,world
    <input type="text" id="model">
    <p id="word"></p>
</body>
<script>
const model = document.getElementById("model")
const word = document.getElementById("word")
const obj= {}

const newObj = new Proxy(obj, {
    get: function(target, key, receiver) {
        console.log(`getting ${key}!`)
        return Reflect.get(target, key, receiver)
    },
    set: function(target, key, value, receiver) {
        console.log('setting',target, key, value, receiver)
        if (key === "text") {
            model.value = value
            word.innerHTML = value
        }
        return Reflect.set(target, key, value, receiver)
    }
});

model.addEventListener("keyup",function(e){
    newObj.text = e.target.value
})
</script>