9.1 代理基础
代理是目标对象的抽象。
9.1.1 创建空代理
空代理,即除了作为一个抽象的目标对象,什么也不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。
代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。
在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象。
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 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
9.1.2 定义捕获器
使用代理的主要目的是可以定义捕获器(trap) 。
捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。
注意: 捕获器(trap) 是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。
const target = {
foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
};
const proxy = new Proxy(target, handler);
9.1.3 捕获器参数和反射 API
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get() 捕获器会接收到目标对象、要查询的属性和代理对象三个参数。
所有捕获器都可以基于自己的参数重建原始操作。
通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来重建。
处理程序对象中所有可以捕获的方法都有对应的 反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。因此,使用反射 API 也可以像下面这样定义出空代理对象:
const target = {
foo: 'bar'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
9.1.4 捕获器不变式
捕获器不变式通常都会防止捕获器定义出现过于反常的行为。使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。
比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:
9.1.5 可撤销代理
Proxy 暴露了 revocable() 方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。
撤销函数(revoke() )是幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出 TypeError。
撤销函数和代理对象是在实例化时同时生成的。
9.1.6 实用反射 API
某些情况下应该优先使用反射 API:
-
反射 API 与对象 API
- 反射 API 并不限于捕获处理程序;
- 大多数反射 API 方法在 Object 类型上有对应的方法。
- 通常 Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。
-
状态标记:很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射 API 方法更有用。
以下反射方法都会提供状态标记:
Reflect.defineProperty()
Reflect.preventExtensions()
Reflect.setPrototypeOf()
Reflect.set()
Reflect.deleteProperty()
-
用一等函数替代操作符
以下反射方法提供只有通过操作符才能完成的操作:
Reflect.get()
:可以替代对象属性访问操作符。Reflect.set()
:可以替代 = 赋值操作符。Reflect.has()
:可以替代 in 操作符或 with()。Reflect.deleteProperty()
:可以替代 delete 操作符。Reflect.construct()
:可以替代 new 操作符。
-
安全地应用函数
9.1.7 代理另一个代理
代理可以拦截反射 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
9.1.8 代理的问题与不足
- 代理中的 this
- 代理与内部槽位
9.2 代理捕获器与反射方法
代理可以捕获 13 种不同的基本操作。这些操作有各自不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式。
对于在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用。不会存在重复捕获的情况。
只要在代理上调用,所有捕获器都会拦截它们对应的反射 API 操作。
9.2.1 get()
get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
console.log('get()');
return Reflect.get(...arguments)
}
});
proxy.foo;
// get()
-
返回值;
-
拦截的操作:
proxy.property
proxy[property]
Object.create(proxy)[property]
Reflect.get(proxy, property, receiver)
-
捕获器处理程序参数:
target
:目标对象。property
:引用的目标对象上的字符串键属性。receiver
:代理对象或继承代理对象的对象。
-
捕获器不变式
如果 target.property 不可写且不可配置,则处理程序返回的值必须与 target.property 匹配。如果 target.property 不可配置且 [[Get]] 特性为 undefined,处理程序的返回值也必须是 undefined。
9.2.2 set()
set() 捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log('set()');
return Reflect.set(...arguments)
}
});
proxy.foo = 'bar';
// set()
- 返回值:返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。
- 拦截的操作
- 捕获器处理程序参数
- 捕获器不变式
9.2.3 has()
has() 捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log('has()');
return Reflect.has(...arguments)
}
});
'foo' in proxy;
// has()
9.2.4 defineProperty()
defineProperty() 捕获器会在 Object.defineProperty() 中被调用。对应的反射 API 方法为Reflect.defineProperty() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
defineProperty(target, property, descriptor) {
console.log('defineProperty()');
return Reflect.defineProperty(...arguments)
}
});
Object.defineProperty(proxy, 'foo', { value: 'bar' });
// defineProperty()
9.2.5 getOwnPropertyDescriptor()
getOwnPropertyDescriptor() 捕获器会在 Object.getOwnPropertyDescriptor() 中被调用。对应的反射 API 方法为 Reflect.getOwnPropertyDescriptor() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
getOwnPropertyDescriptor(target, property) {
console.log('getOwnPropertyDescriptor()');
return Reflect.getOwnPropertyDescriptor(...arguments)
}
});
Object.getOwnPropertyDescriptor(proxy, 'foo');
// getOwnPropertyDescriptor()
9.2.6 deleteProperty()
deleteProperty() 捕获器会在 delete 操作符中被调用。对应的反射 API 方法为 Reflect.deleteProperty() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
deleteProperty(target, property) {
console.log('deleteProperty()');
return Reflect.deleteProperty(...arguments)
}
});
delete proxy.foo
// deleteProperty()
9.2.7 ownKeys()
ownKeys() 捕获器会在 Object.keys() 及类似方法中被调用。对应的反射 API 方法为 Reflect. ownKeys() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
ownKeys(target) {
console.log('ownKeys()');
return Reflect.ownKeys(...arguments)
}
});
Object.keys(proxy);
// ownKeys()
9.2.8 getPrototypeOf()
getPrototypeOf() 捕获器会在 Object.getPrototypeOf() 中被调用。对应的反射 API 方法为Reflect.getPrototypeOf() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
getPrototypeOf(target) {
console.log('getPrototypeOf()');
return Reflect.getPrototypeOf(...arguments)
}
});
Object.getPrototypeOf(proxy);
// getPrototypeOf()
9.2.9 setPrototypeOf()
setPrototypeOf() 捕获器会在 Object.setPrototypeOf() 中被调用。对应的反射 API 方法为 Reflect.setPrototypeOf() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
setPrototypeOf(target, prototype) {
console.log('setPrototypeOf()');
return Reflect.setPrototypeOf(...arguments)
}
});
Object.setPrototypeOf(proxy, Object);
// setPrototypeOf()
9.2.10 isExtensible()
isExtensible() 捕获器会在 Object.isExtensible() 中被调用。对应的反射 API 方法为Reflect.isExtensible() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
isExtensible(target) {
console.log('isExtensible()');
return Reflect.isExtensible(...arguments)
}
});
Object.isExtensible(proxy);
// isExtensible()
9.2.11 preventExtensions()
preventExtensions() 捕获器会在 Object.preventExtensions() 中被调用。对应的反射 API 方法为 Reflect.preventExtensions() 。
const myTarget = {};
const proxy = new Proxy(myTarget, {
preventExtensions(target) {
console.log('preventExtensions()');
return Reflect.preventExtensions(...arguments)
}
});
Object.preventExtensions(proxy);
// preventExtensions()
9.2.12 apply()
apply() 捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply() 。
const myTarget = () => {};
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList) {
console.log('apply()');
return Reflect.apply(...arguments)
}
});
proxy();
// apply()
9.2.13 construct()
construct() 捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct() 。
const myTarget = function() {};
const proxy = new Proxy(myTarget, {
construct(target, argumentsList, newTarget) {
console.log('construct()');
return Reflect.construct(...arguments)
}
});
new proxy; // construct()
9.3 代理模式
9.3.1 跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过
9.3.2 隐藏属性
代理的内部实现对外部代码是不可见的
9.3.3 属性验证
因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值
9.3.4 函数与构造函数参数验证
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。
9.3.5 数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。