Proxy
ES6 中新增的 Proxy 特性为开发者提供了一种实现元编程的“大杀器”。
元编程概念可以简单地描述为:一个程序可以对另外一个程序进行读取、转换,甚至在这第二个程序运行的时候对其进行修改。
Proxy 让我们可以对任何对象的绝大部分行为进行监听和干涉,实现更多的自定义程序行为。
目录:
使用语法
用法:new Proxy(target, handler)
。
与 Getter/Setter 不同的是,Proxy 并不是以语法的形式使用,Proxy 通过设置行为监听方法来捕获程序对对应对象的行为。
const obj = {};
const proxy = new Proxy(obj, {
// ...
})
Proxy 的构造器接受两个参数,第一个参数为需要进行包装的目标对象,第二个参数则为用于监听目标对象行为的监听器,其中监听器可以接受一些参数以监听相对应的程序行为。
属性值 | 监听器参数 | 监听内容 |
---|---|---|
has | (target, prop) | 监听 in 语句的使用 |
get | (target, prop, reciver) | 监听目标对象的属性读取 |
set | (target, prop, value, reciver) | 监听目标对象的属性赋值 |
deleteProperty | (target, prop) | 监听 delete 语句对目标对象的删除属性行为 |
ownKeys | (target) | 监听 Object.getOwnPropertyName() 的读取 |
apply | (target, thisArg, arguments) | 监听目标函数(作为目标对象)的调用行为 |
construct | (target, arguments, newTarget) | 监听目标构造函数(作为目标对象)利用 new 而生成实例的行为 |
getPrototypeOf | (target) | 监听 Objext.getPrototypeOf() 的读取 |
setPrototypeOf | (target, prototype) | 监听 Objext.setPrototypeOf() 的调用 |
isExtensible | (target) | 监听 Objext.isExtensible() 的读取 |
preventExtensions | (target) | 监听 Objext.preventExtensions() 的读取 |
getOwnPropertyDescriptor | (target, prop) | 监听 Objext.getOwnPropertyDescriptor() 的调用 |
defineProperty | (target, property, descriptor) | 监听 Object.defineProperty() 的调用 |
has
可以通过为 Proxy 的 handler 定义 has 监听方法,来监听程序通过 in 语句来检查一个字符串或数字是否为该 Proxy 的目标对象中某个属性的属性键的过程。
const p = new Proxy({}, {
has(target, prop){
console.log(`Checking "${prop}" is in the target or not`);
return true;
}
})
console.log('foo' in p);
// Checking "foo" is in the target or not
// true
该监听方法有两个需要注意的地方,如果遇到这两种情况,便会抛出 TypError 错误。
1.当目标对象被其他程序通过 Object.preventExtensions() 禁用了属性拓展(该对象无法再增加新的属性,只能对当前已有的属性进行操作,包括读取、操作和删除,但是一旦删除就无法再定义)功能,且被检查的属性键确实存在与目标对象中,该监听方法便不能返回 false。
const obj = {foo: 1};
Object.preventExtensions(obj);
const p = new Proxy(obj, {
has(target, prop){
console.log(`Checking "${prop}" is in the target or not`);
return false;
}
})
console.log('foo' in p);
// TypeError: 'has' on proxy: trap returned falsish for property 'foo' but the proxy target is not extensible
2.当被检查的属性键存在与目标对象中,且该属性的 configurable 配置是 false 时,该监听方法不能返回 false。
const obj = {};
Object.defineProperty(obj, 'foo', {
configurable: false,
value: 10
})
const p = new Proxy(obj, {
has(target, prop){
console.log(`Checking "${prop}" is in the target or not`);
return false;
}
})
console.log('foo' in p);
// TypeError: 'has' on proxy: trap returned falsish for property 'foo' which exists in the proxy target as non-configurable
get
Getter只能对已知的属性键进行监听,而无法对所有属性读取行为进行拦截,而 Proxy 可以通过设定 get 监听方法,拦截和干涉目标对象的所有属性读取行为。
const obj = {foo: 1};
const p = new Proxy(obj, {
get(target, prop){
console.log(`Program is trying to fetch the property "${prop}".`);
return target[prop];
}
})
p.foo; // Program is trying to fetch the property "foo".
p.something; // Program is trying to fetch the property "something".
这个监听方法也存在需要注意的地方——当目标对象被读取属性的 configurable 和 writable 属性都为 false 时,监听方法最后返回的值必须与目标对象的原属性值一致。
const obj = {};
Object.defineProperty(obj, 'foo', {
configurable: false,
enumberable: false,
value: 10,
writable: false
})
const p = new Proxy(obj, {
get(target, prop){
return 20;
}
})
console.log(p.foo);
// 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 (expected '10' but got '20')
set
handler.set 用于监听目标对象的所有属性赋值行为。
const obj = {};
const p = new Proxy(obj, {
set(target, prop, value){
console.log(`Setting value "${value}" on the key "${prop}" in the target object`);
target[prop] = value;
return true;
}
})
p.foo = 1;
// Setting value "1" on the key "foo" in the target object
注意,如果目标对象自身的某个属性是不可写也不可配置的,那么 set 不得改变这个属性的值,只能返回同样的值,否则报错。
apply
handler.apply , Proxy 也为作为目标对象的函数提供了监听其调用行为的属性。
const sum = function(...args){
return args
.map(Number)
.filter(Boolean)
.reduce((a, b) => a + b);
}
const p = new Proxy(sum, {
apply(target, thisArg, args){
console.log(`Function is being called with arguments [${args.join()}] and context ${thisArg}`);
return target.call(thisArg, ...args);
}
})
console.log(p(1, 2, 3));
// Function is being called with arguments [1,2,3] and context undefined
// 6
construct
handler.construct, Proxy 也可以将类作为目标监听对象,并监听其通过 new 语句来生产新实例的行为,这同样可以使用再作为构造器的构造函数上。
class Foo{};
const p = new Proxy(Foo, {
construct(target, args, newTarget){
return {arguments: args} // 这里返回的结果会是 new 所得到的实例}
})
const obj = new p(1, 2, 3);
console.log(obj.arguments); // [1, 2, 3]
创建可解除 Proxy 对象
用法:Proxy.revocable(target, handler) : (proxy, revoke)
。
const obj = {foo: 10};
const revocable = Proxy.revocable(obj, {
get(target, prop){
return 20;
}
})
const proxy = revocable.proxy;
console.log(proxy.foo); // 20
revocable.revoke();
console.log(proxy.foo);
// TypeError: Cannot perform 'get' on a proxy that has been revoked
Proxy.revocable(target, handler) 会返回一个带有两个属性的对象,其中一个 proxy 便是该函数所生成的可解除 Proxy 对象,而另一个 revoke 则是将刚才的 Proxy 对象解除的解除方法。
使用场景
看似“不可能”的自动填充
class Tree{
constructor(){
return new Proxy({}, {
get(target, prop){
if(!(prop in target)) target[prop] = new Tree();
return target[prop];
}
})
}
}
const tree = new Tree();
tree.brance1.brance2.leaf = 1;
tree.brance1.brance2.brance3.leaf = 2;
只读试图
通过 Proxy 来对渲染数据对象的行为进行监听和干涉,对所有可能对渲染数据做出修改的行为进行拦截。
const NOPE = () => {
throw new TypeError('Cannot modify the readonly data.');
}
function readonly(data){
return new Proxy(data, {
set: NOPE,
deleteProperty: NOPE,
setPrototypeOf: NOPE,
preventExtensions: NOPE,
defineProperty: NOPE
})
}
const data = {foo: 10};
const readonlyData = readonly(data);
readonlyData.foo = 2; // TypeError: Cannot modify the readonly data.
上面的方法并没有对深层结果进行修改拦截,即 readonlyData.foo.bar = 2; 是不会被拦截的,而且对表层数据的 get 行为也没有拦截。
我们可以对其进行修改,使其实现真正的“只读视图”:
function readonly(data){
return new Proxy(data, {
get(target, prop){
const result = target[prop];
// 判断是否为引用类型
if(Object(result) === result){
return readonly(result);
}
return result;
},
// ...
})
}
const data = {foo: {bar: 1}};
const readonlyData = readonly(data);
readonlyData.foo.bar = 2;
//TypeError: Cannot modify the readonly data.
入侵式测试框架
开发者可以通过使用 Proxy 在定义方法和逻辑代码之间建立一个隔离层,这样便可以在两个地方都无须做大量修改的情况下,实现一下非常深入的测试,从而开发出一个针对业务的入侵式测试框架。
我们通过 Proxy 对目标代码进行包装,比如我们对一些目标 API 方法(函数)打一个“钩子”(Hook),通过一些包装,可以对一些方法进行计时,此处使用 console.time 来进行简单的计时。
import api from './api';
export default hook(api, {
methodName(fn){
return funciton(..args){
console.time('methodName');
return fn(...arga)
.then((...args) => {
console.timeEnd('methodName');
return Promise.resolve(...args);
})
}
}
})
为了实现这样的“钩子”机制,我们可以对目标对象的 get 行为进行监听和干涉,一旦发现属性键所对应的属性值为一个函数(方法),且在测试案例中存在相应的干涉机制,便将其传递到测试案例中。
function hook(obj, cases){
return new Proxy(obj, {
get(target, prop){
if((prop in cases) && (typeof target[prop] === 'function')){
const fn = target[prop];
return cases[prop](fn);
}
return target[prop];
}
})
}