ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力,也就是可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。
从很多方面说,代理类似C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。
创建空代理
代理使用Proxy
构造函数创建,其接受两个参数:目标对象和处理程序对象。两个参数缺一不可,否则会报TypeError
。
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
// 给代理属性赋值会反映在两个对象上,因为这个赋值会转移到目标对象
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 操作符
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
定义捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
定义一个 get()捕获器
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()捕获器。注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
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
捕获器参数和反射API
所有捕获器都能访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
console.log(trapTarget === target); // true,目标对象
console.log(property); // foo,要查询的属性
console.log(receiver === proxy); // true,代理对象
}
};
const proxy = new Proxy(target, handler);
proxy.foo; // 调用foo属性,即会调用get,所以会打印三条语句
// true
// foo
// true
有了这些参数,我们可以进行捕获拦截原始行为,即在调用get方法前,可以拦截,做些其他的事情
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
由于这种通过手动写代码重建原始操作较为繁琐,并且并非所有捕获器都如get()那么简单,所以JavaScript提供了全局Reflect对象(封装了原始行为)的同名方法来轻松重建。
定义空代理对象
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
也可以不定义处理程序对象
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
捕获器不变式
捕获器不变式是指不是每个对象都能改变其基本方法,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出TypeError:
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);
// TypeError