语法
Proxy 是一个构造函数,接收两个参数:原对象和捕捉器。Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
const p = new Proxy(target, handler)
target: 任何类型的对象
handler:捕捉器,可以代理捕获 13 种操作(具体种类见附录),所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
先来看一个最简单的示例
const object = { name: 'king', age: 18, }; const proxy = new Proxy(object, { get(target, key, receiver) { return target[key]; }, set(target, key, newValue, receiver) { console.log(key, 'setting new value:', newValue); target[key] = newValue; return true; // set 捕获器需要返回 boolean 表示操作是否成功 }, }); console.log(proxy.name); proxy.name = 'queen'; console.log(proxy.name); // king // name setting new value: queen // queen 复制代码
大名鼎鼎的 Vue3 核心原理就是基于 Proxy 来实现的,在 get 捕获器中收集依赖,在 set 捕获器中触发依赖,具体原理就不在此赘述了。
但是其实这不是最规范的用法,规范的用法应该是配合反射来完成代理
const proxy = new Proxy(object, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, set(target, key, newValue, receiver) { console.log(key, 'setting new value:', newValue); return Reflect.set(target, key, newValue, receiver); }, }); 复制代码
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。Reflect 提供了13 种静态方法,对应 Proxy中 handler 的 13 中捕获器。
捕获器不变式
捕获器拥有改变所有基本方法的能力,但不是没有限制。即捕获器不能违反属性描述,例如
const o = {}; Object.defineProperty(o, 'name', { value: 'queen', writable: false, }); const p = new Proxy(o, { get() { return 'king'; }, }); console.log(p.name); 复制代码
这段代码首先定义了一个空对象,然后使用 Object.defineProperty 定义了一个 name 属性并且设置了值不可写,即这个值是只读的,但是在 proxy 中却返回了不一样的值,这就会抛出 TypeError
receiver
有的人可能已经注意到了上面的示例中get 和 set 都有一个 receiver 参数,这个参数一般不会使用,但在某些情况下,他会发挥很大的作用。它的指向是Proxy 或者继承 Proxy 的对象。
在一般情况下,receiver 指向的是 Proxy 对象
const objRec = { name: 'king', }; const pRec = new Proxy(objRec, { get(target, key, receiver) { if (receiver === pRec) { console.log('receiver is proxy object.'); } return Reflect.get(target, key); }, }); console.log(pRec.name); // receiver is proxy object. // king 复制代码
但是如果有个对象继承了这个 proxy,就会是另一种情况
const objRec = { name: 'king', }; const objExt = {} as { name: string }; const pRec = new Proxy(objRec, { get(target, key, receiver) { if (receiver === pRec) { console.log('receiver is proxy object.'); } if (receiver === objExt) { console.log('receiver is extends object.'); } return Reflect.get(target, key); }, }); console.log(pRec.name); Object.setPrototypeOf(objExt, pRec); console.log(objExt.name); // receiver is proxy object. // king // receiver is extends object. // king 复制代码
可以看到当有对象继承自 proxy 对象时,这个对象访问自身不存在的属性时,就会访问到 proxy 对象,此时 receiver 就指向就不再是 proxy对象了。
上面看完了 proxy 的 receiver,下面再来看一下 Reflect 的 receiver
如果
target
对象中指定了getter
,receiver
则为getter
调用时的this
值。
上面是 MDN 中对 Reflect.get 的 receiver 描述,可能会有点绕,那我们从例子中来理解一下
const obj = { name: 'king', get value() { return this.name; }, }; const objExt = { name: 'queen' } as { value: string; name: string }; const p = new Proxy(obj, { get(target, key) { return Reflect.get(target, key); }, }); console.log(p.value); // king Object.setPrototypeOf(objExt, p); console.log(objExt.value); // king 复制代码
第一个 king 不难理解,p.value,因为 p 是 obj 的代理对象,所以获取到了obj 的 value 值。按照 this 的指向,第二个值是 objExt 调用,按照 this 的指向,这里的值应该是 queen。
这里可以看这段代码
const a = { name: 'zhang', get value() { return this.name; }, }; const b = { name: 'li' } as { value: string; name: string }; Object.setPrototypeOf(b, a); console.log(b.value); // li 复制代码
回到之前的问题,为什么第二个打印的结果是 king 而不是 queue,这里其实是因为 Proxy 的 get 的值始终返回的是 target[key],而 target 是固定的,所以 this 的指向就在这里被偷偷改变了。
要解决这个问题就需要再 Reflect 的参数中加入 receiver,再回味一下之前的那局话
如果target对象中指定了getter,receiver则为getter调用时的this值。
const objRec = { name: 'king', get value() { return this.name; }, }; const objExt = { name: 'queen' } as { value: string; name: string }; const pRec = new Proxy(objRec, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, }); console.log(pRec.value); // king Object.setPrototypeOf(objExt, pRec); console.log(objExt.value); // queen 复制代码
此时的结果就是正常的了,所以,当有需要使用的 this 计算值的时候,最好在反射 API 中传入 receiver
可撤销的代理
Proxy 还提供了一个静态方法Proxy.revocable()用于创建可撤销的代理对象。
Proxy.revocable(target, handler)
const o = { name: 'king' }; const p = Proxy.revocable(o, { get(target, key) { return Reflect.get(target, key); }, }); console.log(p); console.log(p.proxy.name); p.revoke(); console.log(p); 复制代码
这里的 p 不再是一个 proxy 对象,而是在外面又包了一层,除了 proxy 对象还提供了一个取消方法,调用该方法之后,proxy 对象将被销毁
附:所有的捕获器类型
- get(target, property, receiver):拦截对象的读取属性操作
- set(target, property, value, receiver):拦截设置属性值操作
- construct(target, argumentsList, newTarget):拦截 new 操作符
- has(target, prop):拦截 in 操作
- apply(target, thisArg, argumentsList):拦截函数的调用
- defineProperty(target, property, descriptor):拦截对象的 defineProperty 操作
- deleteProperty(target, property):拦截对象的 delete 操作
- getOwnPropertyDescription(target, prop):拦截Object.getOwnPropertyDescriptor()
- getPrototyprOf(target):拦截读取代理对象的原型的操作
- setPropertyOf(target, prototype):拦截 Object.setPrototypeOf()
- isExtensible(target):拦截对对象的 Object.isExtensible()
- ownKeys(target):拦截对象的keys 迭代
- preventExtensions(target):拦截对Object.preventExtensions()