JS - Proxy 代理捕获器原则 & 代理撤销
ES6 新增的代理(Proxy)与反射(Reflect)为开发者提供了拦截并向基础操作嵌入额外行为的能力,具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用,即在对目标对象进行各种操作之前,可以在代理对象中对这些操作加以控制
⚠️: 由于代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转化为之前的 ECMAScript 代码,而代理的行为实际上无可替代,换句话说,代理只能在 百分之一百 支持他们的平台上使用
说明: 每一个拦截器都与之对应着同名的 Reflect API
思维导图
代理基础
代理是目标对象的抽象,可以作为目标对象的替身,有完全独立于目标对象,目标对象可以直接被操作,也可以通过代理来操作,即代理对象傻姑娘执行的任何操作实际上都会应用到目标对象,唯一可感知的不同就是代码中操作的是代理对象;但是代理对象和目标对象访问的不是同一个地址
const target = {
id: "id15",
};
const handlder = {};
const proxy = new Proxy(target, handlder);
// 代理对象和目标对象访问的属性是同名的时候,本质访问的就是同一个值,
// 给目标对象 或者代理对象 属性赋值也会反映到两个对象上
log(proxy.id === target.id); // log: true
target.id = "id_15";
log(proxy.id); // log: id_15
log(target.id); // log: id_15
proxy.id = "pro_id";
log(proxy.id); // log: pro_id
log(target.id); // log: pro_id
// Proxy.rototype 是 undefiend,所以不能使用 instanceof 操作符
log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
代理类和函数
const fn = (...arg) => log(...arg);
// 此时返回的 proxyFn 是一个代理函数
const proxyFn = new Proxy(fn, {
apply(target, thisArg, ...argList) {
log("apply_", target, thisArg, ...argList);
return Reflect.apply(...arguments);
},
});
// 触发 apply 拦截器
proxyFn(1, 2, 3); // log: 1 2 3
class Constructor {
constructor(name) {
this.name = name;
}
}
// 此时返回的 ProxyClass 是一个代理 类
const ProxyClass = new Proxy(Constructor, {
construct(target, argList, newTarget) {
log("construct_", target, argList, newTarget);
return Reflect.construct(...arguments);
},
});
// 触发 construct 拦截器
const c = new ProxyClass("jakequc");
log(c.name); // jakequc
代理类
有时候为了防止代理实例的时候出现 this 的问题,可以将类作为代理
class User {
constructor(name) {
this.name = name;
}
say() {
log(`hi, my name is ${this.name}`);
}
}
const UserClassPrxoy = new Proxy(User, {});
const proxyUser = new UserClassPrxoy("kj");
proxyUser.say(); // log: hi, my name is kj
捕获器
使用代理的主要目的是可以自定义捕获器(trap)。捕获器就是在处理程序对象中定义的拦截器,对每个处理程序对象可以定义 0 个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
所有的捕获器都可以基于自己的参数重建原始操作,可以通过调用全局的 Reflect 对象上(封装了原始行为)的同名方法来轻松重建,即处理程序对象中所有可以捕获的方法都有对应的 Reflect API 方法,这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与拦截方法相同的行为
const proxy = new Proxy(
{ name: "jakequc" },
{
// 捕获器在处理程序对象中以方法名为键
// proxy.property / proxy[property] 等获取属性的时候会出发 get 捕获器
get(target, key, receiver) {
// target 是代理的目标对象,key 获取时的 键(属性),receiver 是代理对象
// log(target, key, receiver);
return target[key] + "__";
},
}
);
log(proxy.name); // log: jakequc__
const reproxy = new Proxy(
{
age: 23,
},
{
// 甚至可以写成 get: Reflect.get
get() {
// 每一个 Proxy 中的拦截器名字 在 Reflect 中都存在,因此可以在 Proxy 拦截器中使用 Reflect
return Reflect.get(...arguments);
},
}
);
log(reproxy.age); // log: 23
如果想创建一个可以有所有捕获方法然后将每个方法转发给对应 Reflect API 的代理,可以将 Proxy 的第二个参数直接传递为 Reflect
const allReflectPro = new Proxy(
{
name: "kj",
},
Reflect
);
log(allReflectPro.name); // log: kj
捕获器不变式
使用捕获器几乎可以改变所有基本方法的行为,但是也有限制;虽然每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”,防止捕获器定义出现过于反常行为;如目标对象有一个不可配置且不可写的数据属性,那么捕获器返回一个与该属性不同的值时,会抛出 TypeError
const target = {};
Object.defineProperty(target, "const_key", {
configurable: false, // 不可删除
writable: false, // 不可写入
value: "love", // const_key 的 值为 love 字符串
});
const proxy = new Proxy(target, {
get() {
// 这里违背了 目标对象 const_key 不可写入(更改)的规则,因此在获取对应的值时会触发 TypeError
return "update_love";
},
});
log(proxy.const_key); // TypeError: 'get' on proxy: property 'const_key' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'love' but got 'update_love')
可撤销代理
使用 new Proxy(target, handler) 创建普通代理来说,这种联系会在代理对象的生命周期内一直持续存在,Proxy.revocable(target,handler) 这个方法返回 revoke 方法可以撤销代理操作,值得注意的是,撤销代理的操作是不可逆的且 revoke 方法是幂等的(即调用多少次结果都一样),撤销代理之后再调用代理会抛出 TypeError;
// 代理对象和撤销代理函数在调用 revocable 在实例化同时生成
const { proxy, revoke } = Proxy.revocable(
{
name: "kj",
},
Reflect
);
log(proxy.name); // log: kj
revoke();
// 撤销代理之后,再使用代理就会报 TypeError 错误
log(proxy.name); // TypeError: Cannot perform 'get' on a proxy that has been revoked
代理的问题与不足
在很大程度上,代理作为对象的虚拟层可以正常使用。但在某些情况下,代理也不能与现在的 ECMAScript 机制更好地协同
代理中的 this
方法中的 this 通常指向调用这个方法的对象, 但是代理对象并不是直接指向 目标对象
const target = {
thisEqual() {
return this === proxy;
},
};
const proxy = new Proxy(target, {});
log(target.thisEqual()); // false, target 和 proxy 不是同一个内存地址
log(proxy.thisEqual()); // true, 此时 this 是 proxy, proxy 当然等于自己
比如 Date 类型方法的执行依赖于 this 值上的内部内部槽位 [[NumberDate]], 而代理对象上不存在这个内部槽位,内部槽位的值也不能通过普通的 get, set 操作访问到,于是代理拦截后本应该转发给目标对象的方法会抛出 TypeError
const target = new Date();
const proxy = new Proxy(target, {});
log(proxy instanceof Date); // log: true
proxy.getDate(); // TypeError: this is not a Date object.
转载自:https://juejin.cn/post/7253437782333538362