核心内容:代理与反射
MDN相关文档
代理
什么是代理?
代理即替某人做某事的行为可以看做是代理,被委托的对象起到一个代理对象的作用,而委托方则是目标对象。遥记得设计模式中也有一种模式即代理模式,当时书上给的例子是,有人想给总经理打电话但这个电话只能通过秘书转接,这个时候秘书就相当于总经理的代理类。
JS中如何实现代理?
通过Proxy(target, handler)
创建代理对象
target: 需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)
简单使用及目标对象和代理对象在调用上的关系
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
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
// hasOwnProperty()方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype
'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype
'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
有些时候可能需要中断代理对象和目标对象的联系,可以通过Proxy.revocable(target, handler);
撤销关联,此撤销存在不可逆
代理有哪些应用场景?
代理本质上是一种对象增强,通过代理这个中间层可以在对目标对象调用的中间添加拦截。在定义代理时传入的第二个参数handler即为包含捕获器(trap)的一个对象,每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
例如,可以定义一个 get()捕获器,在 ECMAScript 操作以某种形式调用 get()时触发。下面的例子定义了一个 get()捕获器:
const target = {
foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
// 所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为
// get(trapTarget, property, receiver), 捕获器可以传递相关参数
};
const proxy = new Proxy(target, handler);
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override
console.log(target['foo']); // bar
console.log(proxy['foo']); // handler override
console.log(Object.create(target)['foo']); // bar
console.log(Object.create(proxy)['foo']); // handler override
通过捕获器trap的使用可以使得我们对目标对象的特定方法进行二次加工处理,比如,插入日志层,调用目标跳转,赋值时属性验证,限制目标类的某些属性被获取,代理这个中间层的添加使得使用方和调用方能解耦的同时保持内部方法的单一职责。
捕获器使用中的编码简化
所有捕获器都可以基于自己的参数重建原始操作,但并非所有捕获器行为都像 get()那么简单。因此,通过手动写码如法炮制的想法是不现实的。实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来轻松重建。
const target = {
foo: 'bar'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
// 更简洁写法
// get: Reflect.get
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
当目标的某个属性是不变式时,即writable: false,在get捕获器中返回与属性值不同的值时,会抛出 TypeError
代理使用上的不足
比如代理中的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);
}
}
由于这个实现依赖 User 实例的对象标识,在这个实例被代理的情况下就会出问题:
const user = new User(123);
console.log(user.id); // 123
const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined
//这是因为User实例一开始使用目标对象作为WeakMap的键,代理对象却尝试从自身取得这个实例
代理与内置引用类型(比如 Array)的实例通常可以很好地协同,但有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错。
一个典型的例子就是 Date 类型。根据 ECMAScript 规范,Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get()和 set()操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:
const target = new Date();
const proxy = new Proxy(target, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: 'this' is not a Date object