JavaScript高级程序设计第四版学习记录-第九章代理与反射

目录

  • 9.1 代理基础
    • 9.1.1 创建空代理 const proxy = new Proxy(target-目标对象, handler-处理程序对象);
    • 9.1.2 定义捕获器
    • 9.1.3 捕获器参数和反射 API - Reflect, 详见9.2节
    • 9.1.4 捕获器不变式
    • 9.1.5 可撤销代理 const { proxy, revoke } = Proxy.revocable(target, handler)
    • 9.1.6 实用反射 API - Reflect.xxx()
      • 1. 反射 API 与对象 API
      • 2. 状态标记
      • 3. 用一等函数替代操作符
      • 4. 安全地应用函数 Reflect.apply()
    • 9.1.7 代理另一个代理
    • 9.1.8 代理的问题与不足
      • 1. 代理中的 this
      • 2. 代理与内部槽位
  • 9.2 代理捕获器与反射方法
    • 9.2.1 get() - 对应Reflect.get()
    • 9.2.2 set() - 对应Reflect.set()
    • 9.2.3 has() - 对应Reflect.has()
    • 9.2.4 defineProperty() - 对应Reflect.defineProperty()
    • 9.2.5 getOwnPropertyDescriptor() - 对应Reflect.getOwnPropertyDescriptor()
    • 9.2.6 deleteProperty() - 对应Reflect.deleteProperty()
    • 9.2.7 ownKeys() - 对应Reflect.ownKeys()
    • 9.2.8 getPrototypeOf() - 对应Reflect.getPrototypeOf(), 取得一个对象的原型
    • 9.2.9 setPrototypeOf() - 对应Reflect.setPrototypeOf() 设置一个对象的原型
    • 9.2.10 isExtensible() - 对应Reflect.isExtensible(), 用于判断一个对象是否可扩展
    • 9.2.11 preventExtensions() - 对应Reflect.preventExtensions(), 判断是否已经不可扩展
    • 9.2.12 apply() - 对应Reflect.apply()
    • 9.2.13 construct() - 对应Reflect.construct()
  • 9.3 代理模式
    • 9.3.1 跟踪属性访问
    • 9.3.2 隐藏属性
    • 9.3.3 属性验证
    • 9.3.4 函数与构造函数参数验证
    • 9.3.5 数据绑定与可观察对象
    • 9.3.6 图灵社区例子演示
  • 9.4 小结

代理和反射是ES6新增的十分重要的元编程特性。
基本操作是指对js的底层操作,包括函数、对象、数组等,如获取属性、修改属性、删除属性、修改对象原型等。
通过代理和反射可以实现对这些操作的拦截和修改。

ECMAScript 6新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。

具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制

对刚刚接触这个主题的开发者而言,代理是一个比较模糊的概念,而且还夹杂着很多新术语。其实只要看几个例子,就很容易理解了。

注意:在 ES6 之前,ECMAScript 中并没有类似代理的特性。由于代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转换为之前的 ECMAScript 代码,因为代理的行为实际上是无可替代的。为此,代理和反射只在百分之百支持它们的平台上有用。可以检测代理是否存在,不存在则提供后备代码。不过这会导致代码冗余,因此并不推荐。

vue2.0响应式双向数据绑定是通过Object.defineProperty()实现的。
vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。
vue2.0的这种响应式方式存在一定的限制性, 不能检测对象property的添加或移除,无法监测到数组下标的变化, 不能通过索引直接设置数组项,不能直接通过length属性修改数组长度。
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第1张图片
因此vue3.0中改为通过代理来实现响应式
ProxyES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。 Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。

因此, 代理是实现前端框架或者其他工具库中的一个十分重要的元编程能力。

9.1 代理基础

正如本章开头所介绍的,代理是目标对象的抽象。

从很多方面看,代理类似 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理施予的行为。

注意 ECMAScript 代理与 C++指针有重大区别,后面会再讨论。不过作为一种有助于理解的类比,指针在概念上还是比较合适的结构。

9.1.1 创建空代理 const proxy = new Proxy(target-目标对象, handler-处理程序对象);

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。

代理是使用Proxy构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出TypeError
要创建空代理,可以传一个简单的对象字面量作为处理程序对象,从而让所有操作畅通无阻地抵达目标对象

如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象

注意, 因为Proxy不是一种普通的数据类型, Proxy.prototypeundefined, 因此不能使用 instanceof 操作符。

// 目标对象target
const target = {
      
 id: 'target' 
}; 
// 处理程序对象handler, 空对象
// 相当于透传代理,在代理对象上执行的所有操作都会无障碍地传播到目标对象
const handler = {
     }; 

// 使用Proxy构造函数创建代理对象proxy
// 接收两个参数:目标对象target 和 处理程序对象handler
// 此处的处理程序对象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 操作符

// target不是通过Proxy通过new创建的,因此不成立
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check 
// instanceof 用于检测实例和构造函数之间的原型继承的关系
// js的继承本质上是基于原型对象的继承, 实例是否是构造函数的实例, 则需要判断实例的原型是否是构造函数的原型对象
// 但Proxy不是一种普通的数据类型,所以Proxy.prototype 是 undefined, 因此不能使用 instanceof 操作符
// personExg._proto_ === Person.prototype
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)是从操作系统中借用的概念。
在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。

例如,可以定义一个get()捕获器,在ECMAScript操作以某种形式调用get()时触发。
下面的例子定义了一个get()捕获器:

const target = {
      
 foo: 'bar' 
}; 
const handler = {
      
 // 捕获器在处理程序对象中以方法名为键
 get() {
      
 	return 'handler override'; 
 } 
}; 
// 使用Proxy构造函数创建代理对象proxy
// 接收两个参数:目标对象target 和 处理程序对象handler
const proxy = new Proxy(target, handler); 

// 在代理对象proxy上执行get操作时,就会触发自定义的get()捕获器
// 在目标对象target上执行get操作仍然会产生正常的行为
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 

当通过代理对象执行 get()相关的操作时,就会触发定义的 get()捕获器。

当然,get()不是ECMAScript对象可以调用的方法。
这个操作在 JavaScript 代码中可以通过多种形式触发并被 get()捕获器拦截到。proxy[property]proxy.propertyObject.create(proxy)[property]等操作都会触发基本的 get()操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发 get()捕获器。

注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为

9.1.3 捕获器参数和反射 API - Reflect, 详见9.2节

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。

比如,get()捕获器会接收到目标对象、要查询的属性和代理对象三个参数。

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; 
// true 
// foo 
// true 

有了这些参数,就可以重建被捕获方法的原始行为:

const target = {
      
 foo: 'bar' 
}; 
// 此处在处理程序对象handler中定义的get方法返回了target目标对象的property属性值
const handler = {
      
 get(trapTarget, property, receiver) {
      
  	console.log(trapTarget, property, receiver); 
 	return trapTarget[property]; 
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第2张图片
所有捕获器都可以基于自己的参数重建原始操作,但并非所有捕获器行为都像get()那么简单。因此,通过手动写码如法炮制的想法是不现实的。

实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来轻松重建
全局Reflect对象上封装了13个方法, 对应js中基本操作,能够拦截和自定义的基本操作就是这13个.
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第3张图片

处理程序对象中所有可以捕获的方法都有对应的反射(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 

甚至还可以写得更简洁一些:

const target = {
      
 foo: 'bar' 
}; 
const handler = {
      
 get: Reflect.get 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar 

事实上,如果真想创建一个可以捕获所有方法,然后将每个方法转发给对应反射 API 的空代理,那么甚至不需要定义处理程序对象, 直接把Reflect对象作为处理程序对象即可:

const target = {
      
 foo: 'bar' 
}; 
const proxy = new Proxy(target, Reflect); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar 

反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法。

比如,下面的代码在某个属性被访问时,会对返回的值进行一番修饰:

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); // bar!!! 
console.log(target.foo); // bar 
console.log(proxy.baz); // qux 
console.log(target.baz); // qux 

9.1.4 捕获器不变式

使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。根据 ECMAScript 规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出TypeError

const target = {
     }; 
Object.defineProperty(target, 'foo', {
      // 此处设置了foo属性不可配置且不可改写 
 configurable: false, 
 writable: false, 
 value: 'bar' 
}); 
const handler = {
      
 get() {
      
 	return 'qux'; 
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); 
// TypeError 

9.1.5 可撤销代理 const { proxy, revoke } = Proxy.revocable(target, handler)

有时候可能需要中断代理对象与目标对象之间的联系。对于使用 new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直持续存在。

Proxy 也暴露了 revocable()方法,这个工厂方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出 TypeError

撤销函数和代理对象是在实例化时同时生成的,如下例中的revokeproxy
通过revocable()工厂方法,在实例化时同时生成撤销函数revoke和代理对象proxy, 即对一个目标对象的代理是可撤销的。

比如,在使用第三方库时,需要传入对象,我们不知道第三方库对这个对象做什么操作,可以传一个可撤销代理,当需要执行的操作执行完成后,可以触发revoke()方法撤销代理,防止第三方库以我们不知道的方式修改实际对象,从而起到隔离和安全的效果。

const target = {
      
 foo: 'bar' 
}; 
const handler = {
      
 get() {
      
 	return 'intercepted'; 
 } 
}; 

// 通过revocable()工厂方法,在实例化时同时生成撤销函数revoke和代理对象proxy
const {
      proxy, revoke } = Proxy.revocable(target, handler); 

console.log(proxy.foo); // intercepted 
console.log(target.foo); // bar 
revoke(); // 执行撤销函数
console.log(proxy.foo); // TypeError 

9.1.6 实用反射 API - Reflect.xxx()

某些情况下应该优先使用反射API,这是有一些理由的。

1. 反射 API 与对象 API

在使用反射API时,要记住:
(1) 反射 API 并不限于捕获处理程序;
(2) 大多数反射 API 方法在 Object 类型上有对应的方法。
通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。

在捕获器中直接调用反射API, 可以直接使用预定义的基本操作完成对代理对象请求的转发。
实际上反射API并不限于捕获处理程序,可以在代码中直接使用。
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第4张图片
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第5张图片
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第6张图片

2. 状态标记

很多反射方法返回称作“状态标记”的布尔值,用于表示执行的操作是否成功。
有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射 API 方法更有用

例如,可以使用反射API对下面的代码进行重构:

使用Object.defineProperty()方法定义对象属性时会返回对象
但使用Reflect.defineProperty()方法定义属性时会返回true/false表示成功/失败

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第7张图片

// 初始代码 
const o = {
     }; 
try {
      
 Object.defineProperty(o, 'foo', 'bar'); // 不符合defineProperty的语法规则而报错
 console.log('success'); 
} catch(e) {
      
 console.log('failure'); 
} 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第8张图片

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第9张图片
在定义新属性时如果发生问题,Reflect.defineProperty()会返回false,而不是抛出错误
因此使用这个反射方法可以这样重构上面的代码:

// 重构后的代码
const o = {
     }; 
if(Reflect.defineProperty(o, 'foo', {
     value: 'bar'})) {
      
 console.log('success'); 
} else {
      
 console.log('failure'); 
} 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第10张图片以下反射方法都会提供状态标记, 即返回值都是布尔值true/false
Reflect.defineProperty()
Reflect.preventExtensions()
Reflect.setPrototypeOf()
Reflect.set()
Reflect.deleteProperty()

3. 用一等函数替代操作符

以下反射方法提供只有通过操作符才能完成的操作。
Reflect.get():可以替代对象属性访问操作符。
Reflect.set():可以替代=赋值操作符。
Reflect.has():可以替代 in 操作符或 with()
Reflect.deleteProperty():可以替代 delete 操作符。
Reflect.construct():可以替代 new 操作符。

4. 安全地应用函数 Reflect.apply()

在通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 属性(虽然可能性极小)。
为绕过这个问题,可以使用定义在 Function 原型上的 apply 方法,比如:

Function.prototype.apply.call(myFunc, thisVal, argumentList); 

这种可怕的代码完全可以使用 Reflect.apply 来避免:

Reflect.apply(myFunc, thisVal, argumentsList); 

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

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第11张图片

9.1.8 代理的问题与不足

代理是在ECMAScript现有基础之上构建起来的一套新API,因此其实现已经尽力做到最好了。很大程度上,代理作为对象的虚拟层可以正常使用。但在某些情况下,代理也不能与现在的ECMAScript机制很好地协同。

1. 代理中的 this

代理潜在的一个问题来源是 this 值。
我们知道,方法中的 this 通常指向调用这个方法的对象:

const target = {
      
 thisValEqualsProxy() {
      
 	return this === proxy; 
 } 
} 
const proxy = new Proxy(target, {
     }); 
console.log(target.thisValEqualsProxy()); // false 
console.log(proxy.thisValEqualsProxy()); // true 

从直觉上讲,这样完全没有问题:调用代理上的任何方法,比如 proxy.outerMethod(),而这个方法进而又会调用另一个方法,如 this.innerMethod(),实际上都会调用 proxy.innerMethod()

多数情况下,这是符合预期的行为。可是,如果目标对象依赖于对象标识,那就可能碰到意料之外的问题。

第 6 章中有一个通过 WeakMap 保存私有变量的例子,以下是它的简化版:

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 的键,代理对象却尝试从自身取得这个实例。
要解决这个问题,就需要重新配置代理,把代理User实例改为代理 User 类本身。之后再创建代理的实例就会以代理实例作为 WeakMap 的键了:

// 把代理`User`实例改为代理 `User` 类本身
const UserClassProxy = new Proxy(User, {
     }); 
const proxyUser = new UserClassProxy(456); 
console.log(proxyUser.id); // 456

2. 代理与内部槽位

代理与内置引用类型(比如 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 
target.getDate(); // 当前日期为几号, 如13号
proxy.getDate(); // TypeError: 'this' is not a Date object

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第12张图片

9.2 代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作。这些操作有各自不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式。

正如前面示例所展示的,有几种不同的JavaScript操作会调用同一个捕获器处理程序。不过,对于在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用。不会存在重复捕获的情况。

只要在代理上调用,所有捕获器都会拦截它们对应的反射API操作。

9.2.1 get() - 对应Reflect.get()

get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()

get()捕获器会接收到目标对象、要查询的属性和代理对象三个参数

const myTarget = {
     }; 
const proxy = new Proxy(myTarget, {
      
 get(target, property, receiver) {
      
 	console.log('get()'); 
 	return Reflect.get(...arguments) 
 } 
}); 
proxy.foo; 
// get() 
  1. 返回值
    返回值无限制。
  2. 拦截的操作
    访问对象属性的四种操作
     proxy.property
     proxy[property]
     Object.create(proxy)[property]
     Reflect.get(proxy, property, receiver)
  3. 捕获器处理程序参数
     target:目标对象。
     property:引用的目标对象上的字符串键属性。
     receiver:代理对象或继承代理对象的对象。

    严格来讲,property 参数除了字符串键,也可能是符号(symbol)键。后面几处也一样

  4. 捕获器不变式
    如果target.property不可写且不可配置( 即configurable: false, writable: false),则处理程序返回的值必须与target.property 匹配。
    如果target.property 不可配置( 即configurable: false) 且[[Get]]特性为undefined,处理程序的返回值也必须是undefined

9.2.2 set() - 对应Reflect.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()
// 'bar' // 返回值

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第13张图片

  1. 返回值
    返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。
  2. 拦截的操作
     proxy.property = value
     proxy[property] = value
     Object.create(proxy)[property] = value
     Reflect.set(proxy, property, value, receiver)
  3. 捕获器处理程序参数
     target:目标对象。
     property:引用的目标对象上的字符串键属性。
     value:要赋给属性的值。
     receiver:接收最初赋值的对象。
  4. 捕获器不变式
    如果 target.property 不可写且不可配置,则不能修改目标属性的值。
    如果 target.property 不可配置且[[Set]]特性为undefined,则不能修改目标属性的值。
    在严格模式下,处理程序中返回false 会抛出 TypeError

9.2.3 has() - 对应Reflect.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() 
// false // 返回值

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第14张图片

  1. 返回值
    has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
  2. 拦截的操作
     property in proxy
     property in Object.create(proxy)
     with(proxy) {(property);}
     Reflect.has(proxy, property)
  3. 捕获器处理程序参数
     target:目标对象。
     property:引用的目标对象上的字符串键属性。
  4. 捕获器不变式
    如果 target.property 存在且不可配置,则处理程序必须返回 true
    如果 target.property 存在且目标对象不可扩展,则处理程序必须返回 true

9.2.4 defineProperty() - 对应Reflect.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() 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第15张图片

  1. 返回值
    defineProperty()必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。
  2. 拦截的操作
     Object.defineProperty(proxy, property, descriptor)
     Reflect.defineProperty(proxy, property, descriptor)
  3. 捕获器处理程序参数
     target:目标对象。
     property:引用的目标对象上的字符串键属性。
     descriptor:包含可选的 enumerable、configurable、writable、value、get 和 set定义的对象。
  4. 捕获器不变式
    如果目标对象不可扩展,则无法定义属性。
    如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性。
    如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性。

9.2.5 getOwnPropertyDescriptor() - 对应Reflect.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() 
// undefined

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第16张图片

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第17张图片

  1. 返回值
    getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回 undefined
  2. 拦截的操作
     Object.getOwnPropertyDescriptor(proxy, property)
     Reflect.getOwnPropertyDescriptor(proxy, property)
  3. 捕获器处理程序参数
     target:目标对象。
     property:引用的目标对象上的字符串键属性。
  4. 捕获器不变式
    如果自有的 target.property 存在且不可配置,则处理程序必须返回一个表示该属性存在的对象。
    如果自有的 target.property 存在且可配置,则处理程序必须返回表示该属性可配置的对象。
    如果自有的 target.property 存在且 target 不可扩展,则处理程序必须返回一个表示该属性存在的对象。
    如果 target.property 不存在且 target 不可扩展,则处理程序必须返回undefined 表示该属性不存在。
    如果 target.property 不存在,则处理程序不能返回表示该属性可配置的对象。

9.2.6 deleteProperty() - 对应Reflect.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() 
// true // 返回值

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第18张图片

  1. 返回值
    deleteProperty()必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。
  2. 拦截的操作
     delete proxy.property
     delete proxy[property]
     Reflect.deleteProperty(proxy, property)
  3. 捕获器处理程序参数
     target:目标对象。
     property:引用的目标对象上的字符串键属性。
  4. 捕获器不变式
    如果自有的 target.property 存在且不可配置,则处理程序不能删除这个属性。

9.2.7 ownKeys() - 对应Reflect.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() 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第19张图片
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第20张图片

  1. 返回值
    ownKeys()必须返回包含字符串或符号的可枚举对象。
  2. 拦截的操作
     Object.getOwnPropertyNames(proxy)
     Object.getOwnPropertySymbols(proxy)
     Object.keys(proxy)
     Reflect.ownKeys(proxy)
  3. 捕获器处理程序参数
     target:目标对象。
  4. 捕获器不变式
    返回的可枚举对象必须包含 target 的所有不可配置的自有属性。
    如果 target 不可扩展,则返回可枚举对象必须准确地包含自有属性键。

9.2.8 getPrototypeOf() - 对应Reflect.getPrototypeOf(), 取得一个对象的原型

getPrototypeOf()捕获器会在 Object.getPrototypeOf()中被调用。对应的反射 API 方法为Reflect.getPrototypeOf()
使用Object.getPrototypeOf()可以取得一个对象的原型.
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第21张图片

const myTarget = {
     }; 
const proxy = new Proxy(myTarget, {
      
 getPrototypeOf(target) {
      
 	console.log('getPrototypeOf()'); 
 	return Reflect.getPrototypeOf(...arguments) 
 } 
}); 
Object.getPrototypeOf(proxy); 
// getPrototypeOf() 
// 返回其原型, 此处即为object

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第22张图片

  1. 返回值
    getPrototypeOf()必须返回对象或 null
  2. 拦截的操作
    Object.getPrototypeOf(proxy)
    Reflect.getPrototypeOf(proxy)
    proxy.__proto__
    Object.prototype.isPrototypeOf(proxy)
    proxy instanceof Object
  3. 捕获器处理程序参数
     target:目标对象。
  4. 捕获器不变式
    如果 target 不可扩展,则 Object.getPrototypeOf(proxy)唯一有效的返回值就是 Object.getPrototypeOf(target)的返回值。

9.2.9 setPrototypeOf() - 对应Reflect.setPrototypeOf() 设置一个对象的原型

setPrototypeOf()捕获器会在 Object.setPrototypeOf()中被调用。对应的反射 API 方法为Reflect.setPrototypeOf()

使用Object.getPrototypeOf()可以设置一个对象的原型.
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第23张图片

const myTarget = {
     }; 
const proxy = new Proxy(myTarget, {
      
 setPrototypeOf(target, prototype) {
      
 	console.log('setPrototypeOf()'); 
 	return Reflect.setPrototypeOf(...arguments) 
 } 
}); 
Object.setPrototypeOf(proxy, Object); 
// setPrototypeOf() 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第24张图片

  1. 返回值
    setPrototypeOf()必须返回布尔值,表示原型赋值是否成功。返回非布尔值会被转型为布尔值。
  2. 拦截的操作
     Object.setPrototypeOf(proxy)
     Reflect.setPrototypeOf(proxy)
  3. 捕获器处理程序参数
     target:目标对象。
     prototype:target 的替代原型,如果是顶级原型则为 null。
  4. 捕获器不变式
    如果 target 不可扩展,则唯一有效的 prototype 参数就是Object.getPrototypeOf(target)的返回值。

9.2.10 isExtensible() - 对应Reflect.isExtensible(), 用于判断一个对象是否可扩展

isExtensible()捕获器会在 Object.isExtensible()中被调用。对应的反射 API 方法为Reflect.isExtensible()

Object.isExtensible()方法用于判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)

const myTarget = {
     }; 
const proxy = new Proxy(myTarget, {
      
 isExtensible(target) {
      
 	console.log('isExtensible()'); 
 	return Reflect.isExtensible(...arguments) 
 } 
}); 
Object.isExtensible(proxy); 
// isExtensible() 
// true // 返回值
  1. 返回值
    isExtensible()必须返回布尔值,表示 target 是否可扩展。返回非布尔值会被转型为布尔值。
  2. 拦截的操作
     Object.isExtensible(proxy)
     Reflect.isExtensible(proxy)
  3. 捕获器处理程序参数
     target:目标对象。
  4. 捕获器不变式
    如果 target 可扩展,则处理程序必须返回 true。
    如果 target 不可扩展,则处理程序必须返回 false。

9.2.11 preventExtensions() - 对应Reflect.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() 
  1. 返回值
    preventExtensions()必须返回布尔值,表示 target 是否已经不可扩展。返回非布尔值会被转型为布尔值。
  2. 拦截的操作
     Object.preventExtensions(proxy)
     Reflect.preventExtensions(proxy)
  3. 捕获器处理程序参数
     target:目标对象。
  4. 捕获器不变式
    如果 Object.isExtensible(proxy)是 false,则处理程序必须返回 true。

9.2.12 apply() - 对应Reflect.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()
// undefined // 返回值
  1. 返回值
    返回值无限制。
  2. 拦截的操作
     proxy(…argumentsList)
     Function.prototype.apply(thisArg, argumentsList)
     Function.prototype.call(thisArg, …argumentsList)
     Reflect.apply(target, thisArgument, argumentsList)
  3. 捕获器处理程序参数
     target:目标对象。
     thisArg:调用函数时的 this 参数。
     argumentsList:调用函数时的参数列表
  4. 捕获器不变式
    target 必须是一个函数对象。

9.2.13 construct() - 对应Reflect.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()
// 返回myTarget对象

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第25张图片

  1. 返回值
    construct()必须返回一个对象。
  2. 拦截的操作
     new proxy(…argumentsList)
     Reflect.construct(target, argumentsList, newTarget)
  3. 捕获器处理程序参数
     target:目标构造函数。
     argumentsList:传给目标构造函数的参数列表。
     newTarget:最初被调用的构造函数。
  4. 捕获器不变式
    target 必须可以用作构造函数。

9.3 代理模式

使用代理可以在代码中实现一些有用的编程模式。

9.3.1 跟踪属性访问

通过捕获 getsethas 等操作,可以知道对象属性什么时候被访问、被查询。
把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:

const user = {
      
 name: 'Jake' 
}; 
const proxy = new Proxy(user, {
      
 get(target, property, receiver) {
      
	 console.log(`Getting ${
       property}`); 
	 return Reflect.get(...arguments); 
 }, 
 set(target, property, value, receiver) {
      
	 console.log(`Setting ${
       property}=${
       value}`); 
	 return Reflect.set(...arguments); 
 } 
}); 
proxy.name; // Getting name 
proxy.age = 27; // Setting age=27 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第26张图片

9.3.2 隐藏属性

代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。

const hiddenProperties = ['foo', 'bar']; // 隐藏起foo, bar这两个属性
const targetObject = {
      
 foo: 1, 
 bar: 2, 
 baz: 3 
}; 
// 对设置隐藏的foo, bar两个属性单独处理
const proxy = new Proxy(targetObject, {
      
 get(target, property) {
      
	 if (hiddenProperties.includes(property)) {
     
	 	return undefined; 
	 } else {
      
	 	return Reflect.get(...arguments); 
	 } 
 }, 
 has(target, property) {
      
	 if (hiddenProperties.includes(property)) {
      
	 	return false; 
	 } else {
      
	 	return Reflect.has(...arguments); 
	 } 
 } 
}); 
// get() 
console.log(proxy.foo); // undefined 
console.log(proxy.bar); // undefined 
console.log(proxy.baz); // 3 
// has() 
console.log('foo' in proxy); // false 
console.log('bar' in proxy); // false 
console.log('baz' in proxy); // true 

9.3.3 属性验证

因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值

const target = {
     
 onlyNumbersGoHere: 0
}; 
const proxy = new Proxy(target, {
      
 set(target, property, value) {
      
	 if (typeof value !== 'number') {
      // 判断所赋的值
	 	return false; 
	 } else {
     
	 	return Reflect.set(...arguments); 
	 } 
 } 
}); 
proxy.onlyNumbersGoHere = 1; 
console.log(proxy.onlyNumbersGoHere); // 1 
proxy.onlyNumbersGoHere = '2'; 
console.log(proxy.onlyNumbersGoHere); // 1 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第27张图片

9.3.4 函数与构造函数参数验证

跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。
比如,可以让函数只接收某种类型的值:

function median(...nums) {
      
 // Math.floor() 返回小于或等于一个给定数字的最大整数。
 // Note:  可以理解 Math.floor()为向下取整
 return nums.sort()[Math.floor(nums.length / 2)]; 
} 
const proxy = new Proxy(median, {
      
 apply(target, thisArg, argumentsList) {
      
	 for (const arg of argumentsList) {
      
		 if (typeof arg !== 'number') {
      
		 	throw 'Non-number argument provided'; 
		 } 
	 } 
	 return Reflect.apply(...arguments); 
 } 
}); 
console.log(proxy(4, 7, 1)); // 4 
console.log(proxy(4, '7', 1)); 
// Error: Non-number argument provided 

类似地,可以要求实例化时必须给构造函数传参:

class User {
      
 constructor(id) {
      
 this.id_ = id; 
 } 
} 
const proxy = new Proxy(User, {
      
 construct(target, argumentsList, newTarget) {
      
	 if (argumentsList[0] === undefined) {
      
	 	throw 'User cannot be instantiated without id'; 
	 } else {
      
	 	return Reflect.construct(...arguments); 
	 } 
 } 
}); 
new proxy(1); 
new proxy(); 
// Error: User cannot be instantiated without id 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第28张图片

9.3.5 数据绑定与可观察对象

通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。

比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:

const userList = []; 
class User {
      
 constructor(name) {
      
 	this.name_ = name; 
 } 
} 
const proxy = new Proxy(User, {
      
 construct() {
      
	 const newUser = Reflect.construct(...arguments); 
	 userList.push(newUser); 
	 return newUser; 
 } 
}); 
new proxy('John'); 
new proxy('Jacob'); 
new proxy('Jingleheimerschmidt'); 
console.log(userList); // [User {}, User {}, User{}] 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第29张图片

另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:

const userList = []; 
function emit(newValue) {
      
 console.log(newValue); 
} 
const proxy = new Proxy(userList, {
      
 set(target, property, value, receiver) {
      
 	 // Reflect.set() 返回true表示设置property为值value成功, false表示失败
	 const result = Reflect.set(...arguments); 
	 if (result) {
     
	 	// 传参Reflect.get()的返回值, 即property属性的value值 
	 	emit(Reflect.get(target, property, receiver)); 
	 } 
	 return result; 
 } 
}); 
proxy.push('John'); 
// John 
proxy.push('Jacob'); 
// Jacob 

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第30张图片

9.3.6 图灵社区例子演示

// 创建一个代理对象a,代理的目标对象是数组,通过reduce方法返回了处理器对象,拦截了所有的代理API方法.
// 在拦截器内部,每个方法都会通过console.log打印出其key值(拦截器的名字/反射api的名字)和参数
var a = new Proxy([], Reflect.ownKeys(Reflect).reduce((handlers, key) => {
     
    handlers[key] = (...args) => {
     
	    console.log(key, ...args)
	    return Reflect[key](...args)
	}
	return handlers
}, {
     }))

通过打印log查看在调用数组的push()方法时, 底层会有哪些基础操作。
a开始是一个空数组,

  1. 通过get方法得到数组的push方法
  2. 通过get方法得到数组的length
  3. 通过set方法设置数组的下标index为0时,其值value为1
    先通过getOwnPropertyDescriptor取得数组第0个索引的属性描述符
    再通过defineProperty定义数组第0个索引的属性,设置value为1,writable、enumerable、configurabletrue
  4. 通过set方法设置数组的length属性为1
    先通过getOwnPropertyDescriptor取得数组length属性描述符
    再通过defineProperty定义数组length属性描述符,设置value为1,其他三个特性writable、enumerable、configurable为默认值false

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第31张图片
理论上在调用 Object.defineProperty()时,configurableenumerablewritable 的值如果不指定,则都默认为 false

但数组的length属性是可以修改的。
通过a.length=2可以设置数组的长度为2。
通过a.length查看数组长度触发了数组的get方法,可以看到数组是[1, empty]
length属性的默认writablefalse相违背,但这其实是数组的特性,数组可以通过修改length属性修改数组的元素长度。
JavaScript高级程序设计第四版学习记录-第九章代理与反射_第32张图片
通过这个例子,可以看到:
1. 通过代理可以分析底层的基础性操作,在常规代码中执行方法调用会有很多相关的基础性操作,这些操作是ECMAScript规范中定义的底层操作,有助于我们理解ECMAScript规范。
2. 数组的length属性是可以修改的,这是数组的特性。尽管在定义时只定义了value,没有指定writable为true。
3. 对比其他对象,调用Object.defineProperty()时,configurableenumerablewritable 的值如果不指定,则都默认为 falselength属性不可修改。

JavaScript高级程序设计第四版学习记录-第九章代理与反射_第33张图片

9.4 小结

代理是 ECMAScript 6 新增的令人兴奋和动态十足的新特性。尽管不支持向后兼容,但它开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地。

从宏观上看,代理是真实 JavaScript 对象的透明抽象层。代理可以定义包含捕获器的处理程序对象,而这些捕获器可以拦截绝大部分 JavaScript 的基本操作和方法。在这个捕获器处理程序中,可以修改任何基本操作的行为,当然前提是遵从捕获器不变式

与代理如影随形的反射 API,则封装了一整套与捕获器拦截的操作相对应的方法。可以把反射 API看作一套基本操作,这些操作是绝大部分 JavaScript 对象 API 的基础。

代理的应用场景是不可限量的。开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。

你可能感兴趣的:(JavaScript高级程序设计第四版学习记录-第九章代理与反射)