正是因为那些看起来很难的努力、付出和坚持,才成就了那个不断变得更好的自己。
今日学习内容
1、JS 红皮书 P266-270 第九章:代理与反射
今日笔记
1、ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。
2、代理基础: 代理是目标对象的抽象。从很多方面看,代理类似 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理施予的行为。
- 创建空代理: 代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出 TypeError。要创建空代理,可以传一个简单的对象字面量作为处理程序对象,从而让所有操作畅通无阻地抵达目标对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const target = { id: 'target' }; const handler = {}; const proxy = new Proxy(target, handler);
console.log(target.id); console.log(proxy.id);
target.id = 'foo'; console.log(target.id); console.log(proxy.id);
proxy.id = 'bar'; console.log(target.id); console.log(proxy.id);
console.log(target.hasOwnProperty('id')); console.log(proxy.hasOwnProperty('id'));
console.log(target instanceof Proxy); 'undefined' in instanceof check console.log(proxy instanceof Proxy); 'undefined' in instanceof check
console.log(target === proxy);
|
- 定义捕获器: 使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
注意 捕获器(trap)是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。
例如,可以定义一个 get()捕获器,在 ECMAScript 操作以某种形式调用 get()时触发。下面的例子定义了一个 get()捕获器:
1 2 3 4 5 6 7 8 9 10
| const target = { foo: "bar", }; const handler = { get() { return "handler override"; }, }; const proxy = new Proxy(target, handler);
|
这样,当通过代理对象执行 get()操作时,就会触发定义的 get()捕获器。当然,get()不是 ECMAScript 对象可以调用的方法。这个操作在 JavaScript 代码中可以通过多种形式触发并被 get()捕获器拦截到。proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都会触发基本的 get()操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发 get()捕获器。注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const target = { foo: "bar", }; const handler = { get() { return "handler override"; }, }; const proxy = new Proxy(target, handler); console.log(target.foo); console.log(proxy.foo); console.log(target["foo"]); console.log(proxy["foo"]); console.log(Object.create(target)["foo"]); console.log(Object.create(proxy)["foo"]);
|
- 捕获器参数和反射 API: 所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get()捕获器会接收到目标对象、要查询的属性和代理对象三个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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;
const target = { foo: "bar", }; const handler = { get(trapTarget, property, receiver) { return trapTarget[property]; }, }; const proxy = new Proxy(target, handler); console.log(proxy.foo); console.log(target.foo);
|
实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来轻松重建。
处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。因此,使用反射 API 也可以像下面这样定义出空代理对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const target = { foo: "bar", }; const handler = { get() { return Reflect.get(...arguments); }, }; const proxy = new Proxy(target, handler); console.log(proxy.foo); console.log(target.foo);
const target = { foo: "bar", }; const handler = { get: Reflect.get, }; const proxy = new Proxy(target, handler); console.log(proxy.foo); console.log(target.foo);
|
事实上,如果真想创建一个可以捕获所有方法,然后将每个方法转发给对应反射 API 的空代理,那么甚至不需要定义处理程序对象:
1 2 3 4 5 6
| const target = { foo: "bar", }; const proxy = new Proxy(target, Reflect); console.log(proxy.foo); console.log(target.foo);
|
反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法。比如,下面的代码在某个属性被访问时,会对返回的值进行一番修饰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const target = { foo: "bar", baz: "qux", }; const handler = { get(trapTarget, property, receiver) { let decoration = ""; if (property === "foo") { decoration = "!!!"; } return Reflect.get(...arguments) + decoration; }, }; const proxy = new Proxy(target, handler); console.log(proxy.foo); console.log(target.foo); console.log(proxy.baz); console.log(target.baz);
|
- 捕获器不变式: 比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const target = {}; Object.defineProperty(target, "foo", { configurable: false, writable: false, value: "bar", }); const handler = { get() { return "qux"; }, }; const proxy = new Proxy(target, handler); console.log(proxy.foo);
|
- 可撤销代理: 有时候可能需要中断代理对象与目标对象之间的联系。对于使用 new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直持续存在。Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出 TypeError。撤销函数和代理对象是在实例化时同时生成的:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const target = { foo: "bar", }; const handler = { get() { return "intercepted"; }, }; const { proxy, revoke } = Proxy.revocable(target, handler); console.log(proxy.foo); console.log(target.foo); revoke(); console.log(proxy.foo);
|