代理与反射
ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。
注意
在ES6 之前,ECMAScript 中并没有类似代理的特性。由于代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转换为之前的ECMAScript 代码,因为代理的行为实际上是无可替代的。为此,代理和反射只在百分之百支持它们的平台上有用。可以检测代理是否存在,不存在则提供后备代码。不过这会导致代码冗余,因此并不推荐。
# 代理基础
在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象。
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
// 在目标对象上定义一个新属性
target.bar = 'bar'
console.log(target.bar) // bar
console.log(proxy.bar) // 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
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
31
32
# 定义捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = {
foo: 'bar'
}
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 捕获器参数和反射API
捕获器将会获得三个参数
- 捕获对象 trapTarget
- 被捕获的属性 property
- 接受捕获的代理 receiver
所有捕获器都可以基于自己的参数重建原始操作,但并非所有捕获器行为都像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
2
3
4
5
6
7
8
9
10
11
更加简化的版本:
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
2
3
4
5
6
7
8
9
10
反射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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 捕获器不变式
使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出TypeError。
# 可撤销代理
const { proxy, revoke } = Proxy.revocable(target, handler);
revoke();
console.log(proxy.foo); // TypeError: Revoked
2
3
# 实用反射 API
# Reflect API 和 Object 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 操作符。
# 安全地应用函数
在通过apply 方法调用函数时,被调用的函数可能也定义了自己的apply 属性(虽然可能性极小)。为绕过这个问题,可以使用定义在Function 原型上的apply 方法,比如:
Function.prototype.apply.call(myFunc, thisVal, argumentList);
使用Reflect.apply() 可以避免这个问题:
Reflect.apply(myFunc, thisVal, argumentsList);
# 代理另一个代理
代理可以拦截反射API 的操作,而这意味着完全可以创建一个代理,通过它去代理另一个代理。这样就可以在一个目标对象之上构建多层拦截网。
# 代理的问题与不足
# 代理中的this
代理潜在的一个问题来源是this 值。我们知道,方法中的this 通常指向调用这个方法的对象:
const target = {
thisValEqualsProxy() {
return this === proxy;
}
}
const proxy = new Proxy(target, {});
console.log(target.thisValEqualsProxy()); // false
console.log(proxy.thisValEqualsProxy()); // true
2
3
4
5
6
7
8
从直觉上讲,这样完全没有问题:调用代理上的任何方法,比如proxy.outerMethod(),而这个方法进而又会调用另一个方法,如this.innerMethod(),实际上都会调用proxy.innerMethod()。多数情况下,这是符合预期的行为。可是,如果目标对象依赖于对象标识,那就可能碰到意料之外的问题。
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
由于这个实现依赖User 实例的对象标识,在这个实例被代理的情况下就会出问题:
const user = new User(123);
const proxy = new Proxy(user, {});
console.log(proxy === user); // false
console.log(proxy.id); // undefined
2
3
4
这是因为User 实例一开始使用目标对象作为WeakMap 的键,代理对象却尝试从自身取得这个实例。要解决这个问题,就需要重新配置代理,把代理User 实例改为代理User 类本身。之后再创建代理的实例就会以代理实例作为WeakMap 的键了:
const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);
2
3
# 代理与内部槽位
代理与内置引用类型(比如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
2
3
4
# 代理捕获器与反射方法
捕获器 | 方法定义 | 目标方法 |
---|---|---|
get(target, property, receiver) | (target: T, propertyKey: PropertyKey, receiver: any) => any | Object.getPrototypeOf() |
set(target, property, value, receiver) | (target: T, propertyKey: PropertyKey, value: any, receiver: any) => boolean | Object.setPrototypeOf() |
has(target, property) | (target: T, propertyKey: PropertyKey) => boolean | in 操作符 |
deleteProperty(target, property) | (target: T, propertyKey: PropertyKey) => boolean | delete 操作符 |
ownKeys(target) | (target: T) => PropertyKey[] | Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.keys() |
getOwnPropertyDescriptor(target, property) | (target: T, propertyKey: PropertyKey) => PropertyDescriptor | Object.getOwnPropertyDescriptors()、Object.getOwnPropertyDescriptor() |
defineProperty(target, property, descriptor) | (target: T, propertyKey: PropertyKey, descriptor: PropertyDescriptor) => boolean | Object.defineProperty()、Object.defineProperties() |
isExtensible(target) | (target: T) => boolean | Object.isExtensible() |
preventExtensions(target) | (target: T) => boolean | Object.preventExtensions() |
getPrototypeOf(target) | (target: T) => object | Object.getPrototypeOf() |
setPrototypeOf(target, prototype) | (target: T, prototype: object) => boolean | Object.setPrototypeOf() |
apply(target, thisArg, argumentsList) | (target: T, thisArg: any, argumentsList: any[]) => any | Function.prototype.apply()、Function.prototype.call() |
construct(target, argumentsList, newTarget) | (target: T, argumentsList: any[], newTarget: any) => object | new 操作符 |
# 代理模式
# 跟踪属性访问
通过捕获get、set 和has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过。
# 隐藏属性
代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。
# 属性验证
因为所有赋值操作都会触发set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值。
# 函数与构造函数参数验证
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值
# 数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。
比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
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{}]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
}
});
proxy.push('John');
// John
proxy.push('Jacob');
// Jacob
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17