可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。 在各种操作影响到目标对象之前,可以在代理对象中对这些操作加以控制。
代理基础
代理是目标对象的抽象。从很多放来看,代理类似C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。
创建空代理
即只作为一个抽象的目标对象,其余什么都不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。
代理是使用Proxy 构造函数创建的,这个构造函数接收两个参数:目标对象和处理程序对象。缺少任一参数都会抛出TypeError错误。
要创建空代理,可以传一个简单的对象字面量作为处理程序对象。
class target = { // 目标对象
id : 'target'
};
const handler = {}; // 处理程序对象
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性/代理属性都会反映在两个对象上,
// 前者因为两个对象访问的是同一个值,
// 后者因为这个赋值会转移到目标对象
target.id = 'cln';
console.log(target.id); // cln
console.log(proxy.id); // cln
proxy.id = 'jk';
console.log(target.id); // jk
console.log(proxy.id); // jk
// hasOwnProperty()方法在两个地方都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
// Proxy.prototype 是undefined,因此不能使用instanceof操作符
console.log(target instanceof Proxy); // TypeError
console.log(proxy instanceof Proxy); // TypeError
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
定义捕获器
使用代理的主要目的是可以定义捕获器,捕获器就是在处理程序对象中定义的基本操作的拦截器。
每个处理程序对象可以包含0或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。
代理可以在这些基本操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = {
foo : 'foo'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
};
const proxy = new Proxy(target,handler);
所有这些操作只要发生在代理对象上,就会触发get()捕获器。 proxy[property]、proxy.property或Object.create(proxy)[property]等操作都会触发基本的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()捕获器会接收到目标对象、要查询的属性和代理对象三个参数
get(trapTarget, property, receiver) {
// 重建被捕获方法的原始行为
return trapTarget[property];
}
};
const proxy = new Proxy(target,handler);
console.log(target.foo); // bar
console.log(proxy.foo); // bar
但并非所有捕获器行为都像get()那么简单,因此手动写码如法炮制的想法是不现实的。 实际上,可以通过全局Reflect对象上(封装了原始行为)的同名方法来轻松重建原始行为。
处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方法,这些方法于捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。 因此,使用反射API也可以定义出空代理对象:
const target = {
foo : 'bar'
};
const handler = {
get (){
return Reflect.get(...arguments);
}
// 简写写法: get : Reflect.get
};
const proxy = new Proxy(target,handler);
console.log(target.foo); // bar
console.log(proxy.foo); // bar
如果真想创建一个可以捕获所有方法,然后将每个方法转发给对应反射API的空代理,那么可以不需要定义处理程序对象:
const target = {
foo : 'bar'
};
const proxy = new Proxy(target,Reflect);
console.log(target.foo); // bar
console.log(proxy.foo); // bar
捕获器不变式
根据ES规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”。 捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。
比如,若目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出TypeError:
const target = {};
Object.definePropertiy(target, 'foo',{
configurable : false,
writable : false,
value : 'bar'
});
const handler = {
get (){
return 'qux';
}
}
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // TypeError
可撤销代理
有时候需要中断代理对象与目标对象之间的联系,Proxy暴露了revocable()方法,支持撤销代理对象与目标对象的关联。
撤销代理的操作是不可逆的,撤销函数(revoke())是幂等的,调用多少次的结果都一样。 撤销代理之后再调用代理会抛出TypeError。
撤销函数和代理对象是在实例化时同时生成的:
const target = {
foo : 'bar'
};
const handler = {
get (){
return 'intercepted';
}
};
const { proxy, revoke } = Proxy.revocable(target, handler); // 使用工厂模式创建
console.log(target.foo); // bar
console.log(proxy.foo); // intercepted
revoke();
console.log(proxy.foo); // TypeError
实用反射API
某些情况下应该优先使用反射API。
- 反射API与对象API 在使用反射API时,要记住:反射API并不限于捕获处理程序;大多数反射API方法在Object类型上有对应的方法。
如反射方法Reflect.ownKeys(Reflect),对应对象方法Object.getOwnPropertiesNames(Reflect),可输出不可枚举元素。
- 状态标记 很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。
const o = {};
if(Reflect.defineProperty(o, 'foo', { value : 'bar' }){
console.log('success');
})else{
console.log('fail');
}
以下反射方法都会提供状态标记:
1)Reflect.defineProperty()
2)Reflect.preventExtensions()
3)Reflect.setPropertyOf()
4)Reflect.set()
5)Reflect.deleteProperty()
- 用一等函数替代操作符 以下反射方法提供只有通过操作符才能完成的操作:
1)Reflect.get()-可替代对象属性访问操作符
2)Reflect.set()-可替代=赋值操作符
3)Reflect.has()-可替代in操作符或with
4)Reflect.deleteProperty()- 可替代delete操作符
5)Reflect.construct()- 可替代new操作符
- 安全地应用函数 在通过apply方法调用函数时,被调用函数可能也定义自己的apply属性(虽然可能性极小)。 可以使用定义在Function原型上的apply方法:
Function.prototype.apply.call(myFunc, thisVal, argumentList); 完全可以使用Reflect.apply来避免: Reflect.apply(myFunc, thisVal, argumentList);
代理另一个代理
代理可以拦截反射API的操作,意味着可以创建一个代理,通过它去代理另一个代理,就可以在一个目标对象之上构建多层拦截网:
const target = { foo : 'bar' };
const firstProxy = new Proxy(target, {
get(){
console.log(firstProxy);
return Reflect.get(...arguments);
}
});
const secondProxy = new Proxy(firstProxy, {
get(){
console.log(secondProxy);
return Reflect.get(...arguments);
}
});
console.log(secondProxy.foo);
// secondProxy
// firstProxy
// bar
代理的问题与不足
-
代理中的this 若目标对象依赖于对象标识,可能会遇到意料之外的问题。
-
代理与内部槽位 有些ES内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法出错。
如Date类型,根据ES规范,Date类型方法的执行依赖this值上的内部槽位[[NumberDate]]。
代理对象不存在这个内部槽位,且此槽位的值不能通过普通的get和set方法操作访问到,于是代理 拦截后本应转发给目标对象的方法会抛出TypeError:
const target = {};
const proxy = new Proxy(target, {});
proxy.getDate(); // TypeError: 'this' is not a Date object
代理捕获器与反射方法
代理可以捕获13中不同的基本操作。
对于在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用。不存在重复捕获的情况。
只要在代理上调用,所有捕获器都会拦截它们对应的反射API操作。
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。
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。
-
拦截的操作 proxy.property = value
proxy[property] = value
Object.create(proxy)[property] = value
Reflect.set(proxy, property, value, receiver)
- 捕获器处理程序参数 target:目标对象;
property:引用的目标对象上的字符串属性;
value : 要赋给属性的值;
receiver:接收最初赋值的对象。
- 捕获器不变式 如果target.property不可写且不可配置,则不能修改目标属性的值。
如果target.property不可配置且[[Set]]特性为undefined,则不能修改目标属性的值。
在严格模式下,处理程序中返回false会抛出TypeError。
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 = 'bar'; // has()
-
返回值 必须返回布尔值,表示属性是否存在,返回的非布尔值会被转型为布尔值。
-
拦截的操作 property in proxy
property in Object.create(proxy)
with(proxy) { {property};}
Reflect.has(proxy, property)
- 捕获器处理程序参数 target:目标对象;
property:引用的目标对象上的字符串属性。
- 捕获器不变式 如果target.property存在且不可配置,则处理程序必须返回true;
如果target.property存在且目标对象不可扩展,则处理程序必须返回true。
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()
-
返回值 必须返回布尔值,表示属性是否成功定义,返回的非布尔值会被转型为布尔值。
-
拦截的操作 Object.defineProperty(proxy, property, descriptor)
Reflect.defineProperty(proxy, property, descriptor)
- 捕获器处理程序参数 target:目标对象;
property:引用的目标对象上的字符串属性;
descriptor : 包括可选的enumerable、configurable、writable、value、get和set定义的对象。
- 捕获器不变式 如果目标对象不可扩展,则无法定义属性。
如果目标属性有一个可配置属性,则不能添加同名的不可配置属性。
如果目标属性有一个不可配置属性,则不能添加同名的可配置属性。
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。
-
拦截的操作 Object.getOwnPropertyDescriptor(proxy, property)
Reflect.getOwnPropertyDescriptor(proxy, property)
- 捕获器处理程序参数 target:目标对象;
property:引用的目标对象上的字符串属性;
- 捕获器不变式 如果自有的target.property存在且不可配置,则处理程序必须返回一个表示该属性存在的对象。
如果自有的target.property存在且可配置,则处理程序必须返回表示该属性可配置的对象。
如果自有的target.property存在且target不可扩展,则处理程序必须返回一个表示该属性存在的对象。
如果target.property不存在且target不可扩展,则处理程序必须返回undefined表示该属性不存在。
如果target.property不存在,则处理程序不能返回表示该属性可配置的对象。
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()
-
返回值 必须返回布尔值,表示删除属性是否成功,返回的非布尔值会被转型为布尔值。
-
拦截的操作 delete proxy.property
delete proxy[property]
Reflect.deleteProperty(proxy, property)
- 捕获器处理程序参数 target:目标对象;
property:引用的目标对象上的字符串属性;
- 捕获器不变式 如果自有的target.property存在且不可配置,则处理程序不能删除这个属性。
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()
-
返回值 必须返回包含字符串或符号的可枚举对象。
-
拦截的操作 Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
Reflect.ownKeys(proxy)
-
捕获器处理程序参数 target:目标对象。
-
捕获器不变式 返回的可枚举对象必须包含target的所有不可配置的自有属性。
如果target不可扩展,则返回的可枚举对象必须准确包含自有属性键。
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()
- 返回值
必须返回对象或null。
- 拦截的操作
Object.getPrototypeOf(proxy)
Reflect.getPrototypeOf(proxy)
proxy.proto
Object.prototype.isPrototypeOf(proxy)
proxy instanceof Object
- 捕获器处理程序参数
target:目标对象。
- 捕获器不变式
如果target不可扩展,则Object.getPrototypeOf(proxy)唯一有效的返回值就是Object.getPrototypeOf(target)的返回值。
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()
- 返回值
必须返回布尔值,表示原型赋值是否成功,返回的非布尔值会被转型为布尔值。
- 拦截的操作
Object.setPrototypeOf(proxy)
Reflect.setPrototypeOf(proxy)
- 捕获器处理程序参数
target:目标对象。
prototype : target的代替原型,如果是顶级原型则为null。
- 捕获器不变式
如果target不可扩展,则唯一有效的prototype参数就是Object.getPrototypeOf(target)的返回值。
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()
- 返回值
必须返回布尔值,表示target是否可扩展,返回非布尔类型会被转型为布尔值。
- 拦截的操作
Object.isExtensible(proxy)
Reflect.isExtensible(proxy)
- 捕获器处理程序函数
target:目标对象。
- 捕获器不变式 如果target可扩展,则处理程序必须返回true;若不可扩展,则返回false。
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()
- 返回值
必须返回布尔值,表示target是否已经不可扩展,返回非布尔类型会被转型为布尔值。
- 拦截的操作
Object.preventExtensions(proxy)
Reflect.preventExtensions(proxy)
- 捕获器处理程序函数
target:目标对象。
- 捕获器不变式 如果Object.isExtensible(proxy)是false,则处理程序必须返回true。
apply()
其捕获器会在调用函数时中被调用,对应反射API方法时Reflect.apply()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList){
console.log('apply()');
return Reflect.apply(...arguments);
}
});
proxy(); // apply()
- 返回值
返回值无限制。
- 拦截的操作
proxy(...argumentList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, ...argumentsList)
Reflect.apply(target, thisArg, ...argumentsList)
- 捕获器处理程序参数
target:目标对象
thisArg:调用函数时的this参数
argumentsList:调用函数时的参数列表
- 捕获器不变式
target必须是一个函数对象。
construct()
其捕获器会在new操作符中被调用,对应反射API方法为Reflect.construct()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
construct(target, argumentsList, newTarget){
console.log('construct()');
return Reflect.construct(...arguments);
}
});
new proxy(); // construct()
- 返回值
必须返回一个对象。
- 拦截的操作
new proxy(...argumentsList)
Reflect.construct(target, argumentsList, newTarget)
- 捕获器处理程序参数
target:目标构造函数
argumentsList:传给目标构造函数的参数列表
newTarget:最初被调用的构造函数
- 捕获器不变式
target 必须可以用作构造函数。
代理模式
跟踪属性访问
通过捕获get、set和has等操作,可以知道对象属性什么时候被访问、被查询。 把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:
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
隐藏属性
代理的内部实现对外部代码是不可见的,因此隐藏目标对象上的属性也很简单。
const hidProperties = ['foo', 'bar'];
const target = {
foo :1,
bar :2,
baz :3
};
const proxy = new Proxy(target, {
get(target, property){
if(hidProperties.includes(property)){
return undefined;
}else {
return Reflect.get(...arguments);
}
},
has(target, property){
if(hidProperties.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
属性验证
所有赋值操作都会涉及set()捕获器,因此可以根据所赋的值决定是允许还是拒绝赋值:
const target = {
numbers : 0
};
const proxy = new Proxy(target, {
set(target, property, value){
if(typeof value !== 'number'){
return false;
}else{
return Reflect.set(...arguments);
}
}
});
proxy.numbers = 2;
console.log(proxy.number); // 2
proxy.numbers = '52';
console.log(proxy.number); // 2
函数与构造函数参数验证
和保护和验证对象属性类似,比如可以让函数只接受某种类型的值:
function median(...nums){
return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
apply(target, thisArg, argumentsList){
for(const args of argumentsList){
if(typeof args !== 'number'){
throw 'Non-number arguments provided' ;
}
}
return Reflect.apply(...arguments);
}
});
console.log(proxy(4,7,1)); // 4
console.log(proxy(4,'7',1)); // Non-number arguments 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(); // User cannot be instantiated without id
数据绑定与可观察对象
通过代理可以将运行时原本不相干的部分联系在一起,比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
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('jk');
new Proxy('cln');
console.log(userList); // [User{},User{}]
还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:
function printOut(newValue){}
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver){
const result = Reflect.set(...arguments);
if(result){
printOut(Reflect.get(target, property, receiver));
}
return result;
}
});
proxy.push('jjk'); // jjk