第9章 代理与反射
第9章 代理与反射
在ES6中新引入的代理和反射是全新的结构,它们使你能够拦截额外的行为并将其填充到语言的基本操作中。更具体地说,可以定义一个与目标对象相关联的代理对象,并且该代理对象可以用作抽象的目标对象,可以在其中控制在实际到达目标对象之前执行各种操作时会发生什么。
代理基础
在许多方面,代理类似于一个C++指针,因为它可以作为它指向的目标对象的替身,但事实上,它与目标对象完全分开。目标对象可以直接或通过代理进行操纵,但直接操纵将规避代理启用的行为。
创建直通Proxy
在其最简单的形式中,代理只能作为抽象的目标对象存在。默认情况下,在代理对象上执行的所有操作都将透明地传播到目标对象。 因此,可以像使用代理对象关联的目标对象的方式和位置那样使用代理对象。
代理创建使用Proxy构造函数,需提供一个目标对象和一个处理程序对象(handler object),否则会抛出TypeError异常。对于简单的直通(passthrough)代理,使用简单的对象字面量处理程序对象将允许所有操作不受阻碍地到达目标对象。
如下所示,在代理上执行的所有操作将有效地应用于目标对象:
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
console.log(target.id); //target
//id属性访问同样的值
console.log(proxy.id); //target
console.log(proxy.id === target.id); //true
//都拥有id属性
console.log(target.hasOwnProperty('id')); //true
console.log(proxy.hasOwnProperty('id')); //true
//都报错
//console.log(proxy instanceof Proxy);
//Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof
check.
//console.log(target instanceof Proxy);
//Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof
check.
console.log(target === proxy); //false
定义陷阱(traps)
代理的主要目的是允许在处理程序对象内定义充当"基本操作拦截器"的陷阱。每个处理程序对象由零个、一个或多个陷阱组成,每个陷阱对应于在代理上可直接或间接调用的基本操作。当这些基本操作在代理对象上调用时,且在目标对象上调用之前,代理将调用陷阱函数替换之,从而允许拦截和修改其行为。
注意:"陷阱"一词是从操作系统世界借用的,其中陷阱是程序流中的同步中断,在返回到原来的程序流之前,该中断会转移处理器执行以执行子例程。
例如,可以定义每当ECMAScript操作以任何形式执行get()时触发的get()陷阱:
const target = {
hey: 'ha'
};
const handler = {
// 陷阱在处理程序对象内部以方法名为键
get() {
return 'handler override';
}
}
const proxy = new Proxy(target, handler);
console.log(target.hey); //ha
console.log(proxy.hey); //handler override
console.log(target['hey']); //ha
console.log(proxy['hey']); //handler override
console.log(Object.create(target)['hey']); // bar
console.log(Object.create(proxy)['hey']); //handler override
当在这个代理对象上调用get()操作时,将调用为get()定义的陷阱函数替换之。当然,get()在ECMAScript对象上不是一个可用的方法。被捕获的get()操作在实际的JavaScript语言中有多种形式,如proxy[property]、proxy.property、或Object.create(proxy)[property]将使用基本的get()操作获取属性。因此,当它们在代理上使用时,所有这些都将调用陷阱函数,且只有代理会使用陷阱处理函数。
陷阱形参Reflect API
所有陷阱都可以访问形参,这可以完全重建陷阱方法的原生行为。例如,get()方法接受一个对目标对象的引用、被查询的属性和一个对代理对象的引用:
const target = {
hey: 'ha'
};
const handler = {
get(trapTarget, property, receiver) {
console.log(trapTarget === target);
console.log(property);
console.log(receiver === proxy);
}
};
const proxy = new Proxy(target, handler);
proxy.hey;
// true
// hey
// true
因此,可以定义一个陷阱处理程序来完全重新创建被捕获方法的行为:
const target = {
hey: 'ha'
};
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.hey); // ha
console.log(target.hey); // ha
这种策略可以针对所有陷阱实现,但并非所有陷阱行为都像 get() 那样易于重新创建;因此,这是一种不切实际的策略。与手动实现被捕获方法的内容不同, 被捕获方法的原生行为被包装在全局Reflect 对象上的同名方法中 。
每个可以在处理程序对象中捕获的方法都有一个相应的Reflect API方法。该方法具有相同的名称和函数签名,并在实施拦截时执行捕获的方法的确切行为。因此,可以仅使用Reflect API来定义直通代理:
const target = {
hey: 'ha'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.hey); // ha
console.log(target.hey); // ha
或者,以更简洁的格式:
const target = {
hey: 'ha'
};
const handler = {
get() {
get: Reflect.get
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.hey); // ha
console.log(target.hey); // ha
如果希望创建一个真正的直通代理来捕获每个可用方法并将每个方法转移到其相应的Reflect API函数,则不需要显式定义处理程序对象:
const target = {
hey: 'ha'
};
const proxy = new Proxy(target, Reflect);
console.log(proxy.hey); // ha
console.log(target.hey); // ha
Reflect 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
陷阱不变量
陷阱提供了广泛的能力来改变几乎任何基本方法的行为,但它们并非没有限制。每个被捕获的方法都知道目标对象环境和陷阱函数签名,并且陷阱处理程序函数的行为必须遵守ECMAScript规范中指定的“陷阱不变量(“trap invariants)”。陷阱不变量因方法而异,但通常它们会防止陷阱定义表现出任何严重意外的行为。
例如,如果目标对象具有不可配置和不可写入的数据属性,如果试图从陷阱返回与目标对象属性不同的值,则会抛出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);
// Uncaught TypeError: 'get' on proxy: property 'foo' is a read-only and non-
configurable data property on the proxy target but the proxy did not return its actual
value
可撤销代理
可能需要禁用代理对象和目标对象之间的关联。对于使用new Proxy()创建的一般代理,这种关联将持续整个代理对象的生命周期。
Proxy公开了一个revocable()方法,该方法提供了额外的revoke函数,可以将代理对象与目标对象分离。撤销代理是不可逆转的。此外,撤销功能是幂等的,如果多次调用,不会产生进一步的影响。撤销后,在代理上调用任何方法都会抛出TypeError。
revoke函数可在代理实例化时获得:
const target = {
foo: 'bar'
};
const handler = {
get() {
return 'intercepted';
}
};
const {
proxy,
revoke
} = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // Uncaught TypeError: Cannot perform 'get' on a proxy that
has been revoked
Reflect API的用法
在某些情况下,有几个理由支持Reflect API。
Reflect API vs. Object API
当深入Reflect API时,请记住:
Reflect API不受限于陷阱处理程序
大多数Reflect API方法在Object类型上也有类似方法
一般来说,对象方法用于一般应用,反射方法用于微调对象控制和操作。
状态标志
许多反射方法返回Boolean,指示他们打算执行的操作是否成功。在某些情况下,这比其他Reflect API方法的行为方式更有用,后者要么返回修改后的对象,要么抛出错误(取决于方法)。 例如,可以使用Reflect API执行重构:
const o = {};
try {
Object.defineProperty(o, 'foo', 'bar');
console.log('success');
} catch(e) {
console.log('failure'); // failure
}
如果定义新属性出现问题,则Reflect.defineProperty将返回false,而不是抛出错误(实测chrome中会报
错)。重构代码如下:
const o = {};
if (Reflect.defineProperty(o, 'foo', {})) {
console.log('success'); // success
} else {
console.log('failure');
}
以下反射方法提供状态标志:
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);
代理一个代理
代理能够拦截Reflect API操作,这意味着完全可以创建一个代理的代理。这允许在单个目标对象之上构建多个迂回层:
const target = {
foo: 'bar'
};
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy');
return Reflect.get(...arguments) + "111";
}
});
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy');
return "666" + Reflect.get(...arguments) + "222";
}
});
console.log(secondProxy.foo);
//分析:对secondProxy执行get()操作,将拦截firstProxy的get(),而执行secondProxy的get(),输出"second proxy";接着执行return "666" + Reflect.get(...arguments) + "222",“666”之后对firstProxy执行get()操作,输出first proxy,接着执行return Reflect.get(...arguments) + "111";最终输出666bar111222
//second proxy
//first proxy
//666bar111222
代理注意事项和缺点
代理是建立在现有ECMAScript基础设施上的全新API。在大多数情况下,代理作为对象的虚拟化层工作得非常好。然而,在某些情况下,代理不能总是与现有的ECMAScript结构无缝整合。
代理内部的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()。但是,如果你的目标依赖于对象身份,可能会遇到意外问题。
WeakMap私有变量实现(第 6 章),此处显示其删减版本:
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实例的对象身份,因此在User实例被代理时会遇到问题:
const user = new User(123);
console.log(user.id); // 123
const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined
代理和内部插槽
通常,内置引用类型的实例可以无缝地与代理一起工作,如Array。但是,某些ECMAScript内置类型依赖代理无法控制的机制。这样做的结果是实例上的某些方法将无法正常工作。
这方面的典型示例是Date类型。根据ECMAScript规范,Date类型在执行方法时依赖于this值上名为[[NumberData]] 的““internal slot”的存在。因为代理上不存在内部槽,并且因为这些内部槽值不是通过正常的ge和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
代理陷阱和反射方法
代理能够捕获十三种不同的基本操作。每个都有对应的Reflect API方法、形参、关联的ECMAScript操作和不变量。
如前所述,几个不同的JavaScript操作可能会调用相同的陷阱处理程序。但是,对于在代理对象上执行的任何单个操作,只会调用一个陷阱处理程序。
所有在代理上调用的陷阱,也会拦截其相应的Reflect API操作。
get()
get() 陷阱在获取属性值的操作中被调用。其对应的Reflect 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() 陷阱在分配属性值的操作中调用。其对应的Reflect 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运算符内调用。其对应的Reflect API方法是Reflect.has()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log('has()');
return Reflect.has(...arguments)
}
});
'foo' in proxy;
// has()
返回值
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()
defineProperty()陷阱在Object.defineProperty()内被调用。其对应的Reflect 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()
返回值
defineProperty()必须返回一个布尔值,指示属性是否成功定义。非布尔返回值将被强制转换为布尔值。
拦截的操作
Object.defineProperty(proxy, property, descriptor)
Reflect.defineProperty(proxy, property, descriptor)
陷阱处理程序形参
target : 目标对象
property : 目标对象上引用的字符串键属性
descriptor : 包含enumerable, configurable, writable, value,get或set等可选定义的对象
陷阱不变量
如果目标对象不可扩展,则无法添加属性。
如果目标对象有可配置属性,则不能添加相同键的不可配置属性。
如果目标对象具有不可配置属性,则无法添加相同键的可配置属性。
getOwnPropertyDescriptor()
getOwnPropertyDescriptor()陷阱在Object.getOwnPropertyDescriptor()内调用。其对应的Reflect API方法是Reflect.getOwnPropertyDescriptor()。
const myTarget = {
hey: "ha"
};
Object.defineProperty(myTarget, "hey", {
configurable: true
});
myTarget.isExtensible = true;
const proxy = new Proxy(myTarget, {
getOwnPropertyDescriptor(target, property) {
return Reflect.getOwnPropertyDescriptor(...arguments);
}
});
console.log(Object.getOwnPropertyDescriptor(proxy, 'hey'));
// {value: "ha", writable: true, enumerable: true, configurable: false}
console.log(Object.getOwnPropertyDescriptor(proxy, 'ha'));
// undefined
返回值
getOwnPropertyDescriptor()必须返回一个对象,如果该属性不存在,则返回undefined。
拦截的操作
property in proxy
property in Object.create(proxy)
with(proxy) {(property);}
Reflect.has(proxy, property)
陷阱处理程序形参
target : 目标对象
property : 目标对象上引用的字符串键属性
陷阱不变量
如果自己的target.property存在,不管target是否可扩展,该处理程序必须返回一个对象以指示该属性存在。
如果自己的target.property不存在,不管target是否可扩展,则处理程序必须返回undefined以指示该属性不存在。
deleteProperty()
deleteProperty()陷阱在delete运算符内调用。其对应的Reflect API方法是Reflect.deleteProperty()。
返回值
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()
ownKeys()陷阱函数在Object.keys()或与之相似的方法中调用,对应的Reflect API方法为Reflect.ownKeys()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
ownKeys(target) {
console.log('ownKeys()');
return Reflect.ownKeys(...arguments)
}
});
Object.keys(proxy);
// ownKeys()
返回值
ownKeys()必须返回包含字符串或symbol的可枚举对象。
拦截的操作
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
Reflect.ownKeys(proxy)
陷阱处理程序形参
- target : 目标对象
陷阱不变量
返回的可枚举对象包含target所有的不可配置自有属性。
如果target不可扩展,返回的可枚举对象必须正确地包含target的自有属性键。
getPrototypeOf()
getPrototypeOf()陷阱在Object.getPrototypeOf()内部调用。其对应的Reflect API方法是Reflect.getPrototypeOf()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
getPrototypeOf(target) {
console.log('getPrototypeOf()');
return Reflect.getPrototypeOf(...arguments)
}
});
Object.getPrototypeOf(proxy);
// getPrototypeOf()
返回值
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()
setPrototypeOf()陷阱在Object.setPrototypeOf()内调用。其对应的Reflect 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()
返回值
setPrototypeOf()必须返回一个布尔值,指示原型分配是否成功。非布尔返回值将被强制转换为布尔值。
拦截的操作
Object.setPrototypeOf(proxy)
Reflect.setPrototypeOf(proxy)
陷阱处理程序形参
target : 目标对象
prototype : target的预期替换原型,如果这是顶级原型,则为null。
陷阱不变量
如果target是不可扩展的,则唯一有效的prototype形参是从Object.getPrototypeOf(target)返回的值。
isExtensible()
isExtensible()陷阱在Object.isExtensible()内调用。其对应的Reflect API方法是 Reflect.isExtensible()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
isExtensible(target) {
console.log('isExtensible()');
return Reflect.isExtensible(...arguments)
}
});
Object.isExtensible(proxy);
// isExtensible()
返回值
isExtensible()必须返回一个布尔值,指示target是否可以扩展。非布尔返回值将被强制转换为布尔值。
拦截的操作
Object.isExtensible(proxy)
Reflect.isExtensible(proxy)
陷阱处理程序形参
- target : 目标对象
陷阱不变量
如果target是可扩展的,则处理程序必须返回true。
如果target是不可扩展的,则处理程序必须返回false。
preventExtensions()
preventExtensions()陷阱在Object.preventExtensions()中被调用。其对应的Reflect API方法是Reflect.preventExtensions()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
preventExtensions(target) {
console.log('preventExtensions()');
return Reflect.preventExtensions(...arguments)
}
});
Object.preventExtensions(proxy);
// preventExtensions()
返回值
preventExtensions()必须返回一个布尔值,指示target是否已经不可扩展。非布尔返回值将被强制转换为布尔值。
拦截的操作
Object.preventExtensions(proxy)
Reflect.preventExtensions(proxy)
陷阱处理程序形参
- target : 目标对象
陷阱不变量
如果Object.isExtensible(proxy)为false,则处理程序必须返回true。
apply()
apply()陷阱在函数调用时被调用。其对应的Reflect API方法是Reflect.apply()。
const myTarget = () => {};
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList) {
console.log('apply()');
return Reflect.apply(...arguments)
}
});
proxy();
// apply()
返回值
返回值不受限制。
拦截的操作
proxy(...argumentsList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, ...argumentsList)
Reflect.apply(target, thisArgument, argumentsList)
陷阱处理程序形参
target : 目标对象
thisArg : 函数调用的this参数
argumentsList : 函数调用的参数列表
陷阱不变量
target必须是函数对象
construct()
construct()陷阱在new操作符内调用,其对应的Reflect 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()
返回值
construct()必须返回一个对象。
拦截的操作
new proxy(...argumentsList)
Reflect.construct(target, argumentsList, newTarget)
陷阱处理程序形参
target : 目标对象
argumentsList : 函数调用的参数列表
newTarget : 最初调用的构造函数
陷阱不变量
target必须能够用作构造函数。
代理模式
Proxy API允许在代码中引入一些非常有用的模式。
跟踪属性访问
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
隐藏属性
代理的内部对远程代码完全隐藏,因此很容易隐藏target对象上属性。例如:
const hiddenProperties = ['foo', 'bar'];
const targetObject = {
foo: 1,
bar: 2,
baz: 3
};
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
属性验证
因为所有赋值都必须通过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
函数和构造函数形参验证
与验证和保护对象属性的方式相同,函数和构造函数形参也可用于审查。例如,一个函数可以确保它只提供某种类型的值:
function median(...nums) {
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
数据绑定和Observables
代理允许将运行时的各个不相关部分交织在一起。这允许多种模式使不同的代码相互交互。
例如,代理类可以绑定到全局实例集合,以便将每个创建的实例添加到该集合中:
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{}]
或者,一个集合可以绑定到一个发射器,每次插入一个新实例时都会触发:
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