《JavaScript高级程序设计》 | 代理是目标对象的抽象

33 阅读8分钟

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 跟踪属性访问

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

9.3.2 隐藏属性

代理的内部实现对外部代码是不可见的

9.3.3 属性验证

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

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

跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。

9.3.5 数据绑定与可观察对象

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