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>