0%

Proxy

代理就是一种由你创建的特殊对象,它“封装”另一个普通对象,或者说挡在这个普通对象前面,你可以在代理对象上注册特殊的处理函数(trap),代理上执行各种操作的时候会调用这个程序

1
2
3
4
5
6
7
8
9
10
11
12
var obj={a:1},
handles={
get(target,key,context){
//target==obj,context==pobj,key为属性名
console.log("accessing",key);
return Reflect.get(target,key,context)
}
}
pobj=new Proxy(obj,handlers);
obj.a;//1
pobj.a;//acessing:a
//1

这里的映射是有意对称的,每个代理处理函数在对应的元编程任务执行的时候进行拦截,而每个Reflefct工具在一个对象上执行相应的元编程任务,每个代理函数都有一个自动调用相应的Reflect工具的默认定义。

一、介绍

定义: 用于定义基本操作的自定义行为

本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作

一段代码来理解

1
2
3
4
5
6
7
1#!/bin/bash
2# metaprogram
3echo '#!/bin/bash' >program
4for ((I=1; I<=1024; I++)) do
5 echo "echo $I" >>program
6done
7chmod +x program

这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行echo,如果我们手动来写1024行代码,效率显然低效

  • 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译

Proxy 亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

二、用法

Proxy为 构造函数,用来生成 Proxy 实例

1
1var proxy = new Proxy(target, handler)

参数

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

handler解析

关于handler拦截属性,有如下:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截Object.keys(proxy)for...in等循环,返回一个数组
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作

Reflect

若需要在Proxy内部调用对象的默认行为,建议使用Reflect,其是ES6中操作对象而提供的新 API

基本特点:

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
  • Object操作都变成函数行为

下面我们介绍proxy几种用法:

get()

get接受三个参数,依次为目标对象、属性名和 proxy 实例本身,最后一个参数可选

1
2
3
4
5
6
7
8
9
10
11
1var person = {
2 name: "张三"
3};
4
5var proxy = new Proxy(person, {
6 get: function(target, propKey) {
7 return Reflect.get(target,propKey)
8 }
9});
10
11proxy.name // "张三"

get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1function createArray(...elements) {
2 let handler = {
3 get(target, propKey, receiver) {
4 let index = Number(propKey);
5 if (index < 0) {
6 propKey = String(target.length + index);
7 }
8 return Reflect.get(target, propKey, receiver);
9 }
10 };
11
12 let target = [];
13 target.push(...elements);
14 return new Proxy(target, handler);
15}
16
17let arr = createArray('a', 'b', 'c');
18arr[-1] // c

注意:如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1const target = Object.defineProperties({}, {
2 foo: {
3 value: 123,
4 writable: false,
5 configurable: false
6 },
7});
8
9const handler = {
10 get(target, propKey) {
11 return 'abc';
12 }
13};
14
15const proxy = new Proxy(target, handler);
16
17proxy.foo
18// TypeError: Invariant check failed

set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身

假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1let validator = {
2 set: function(obj, prop, value) {
3 if (prop === 'age') {
4 if (!Number.isInteger(value)) {
5 throw new TypeError('The age is not an integer');
6 }
7 if (value > 200) {
8 throw new RangeError('The age seems invalid');
9 }
10 }
11
12 // 对于满足条件的 age 属性以及其他属性,直接保存
13 obj[prop] = value;
14 }
15};
16
17let person = new Proxy({}, validator);
18
19person.age = 100;
20
21person.age // 100
22person.age = 'young' // 报错
23person.age = 300 // 报错

如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1const obj = {};
2Object.defineProperty(obj, 'foo', {
3 value: 'bar',
4 writable: false,
5});
6
7const handler = {
8 set: function(obj, prop, value, receiver) {
9 obj[prop] = 'baz';
10 }
11};
12
13const proxy = new Proxy(obj, handler);
14proxy.foo = 'baz';
15proxy.foo // "bar"

注意,严格模式下,set代理如果没有返回true,就会报错

1
2
3
4
5
6
7
8
9
10
11
1'use strict';
2const handler = {
3 set: function(obj, prop, value, receiver) {
4 obj[prop] = receiver;
5 // 无论有没有下面这一行,都会报错
6 return false;
7 }
8};
9const proxy = new Proxy({}, handler);
10proxy.foo = 'bar';
11// TypeError: 'set' on proxy: trap returned falsish for property 'foo'

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1var handler = {
2 deleteProperty (target, key) {
3 invariant(key, 'delete');
4 Reflect.deleteProperty(target,key)
5 return true;
6 }
7};
8function invariant (key, action) {
9 if (key[0] === '_') {
10 throw new Error(`无法删除私有属性`);
11 }
12}
13
14var target = { _prop: 'foo' };
15var proxy = new Proxy(target, handler);
16delete proxy._prop
17// Error: 无法删除私有属性

注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错

Reflect.apply()

静态方法 **Reflect**.apply() 通过指定的参数列表发起对目标(target)函数的调用。

1
Reflect.apply(target, thisArgument, argumentsList)

参数

  • target

    目标函数。

  • thisArgument

    target函数调用时绑定的this对象。

  • argumentsList

    target函数调用时传入的实参列表,该参数应该是一个类数组的对象。

返回值

返回值是调用完带着指定参数和 this 值的给定的函数后返回的结果。

可取消代理

普通代理总是陷入到目标对象,并且在创建之后不能修改——只要还保持着对这个代理的引用,代理的机制就将维持下去,如果你想要的创建一个在你想要停止它作为代理时便可以被停止的代理,可以创建可取消代理。(revocable proxy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj={a:1},
handlers={
get(target,key,context){
console.log('accessing:',key);
return target[key];
}
};
var p=Proxy.revocable(obj,handlers);//返回一个有两个属性proxy和revoke的对象
const pobj=p.proxy;
const prevoke=p.revoke;
console.log(pobj.a);
prevoke();//代理被取消后,任何对它的访问都会抛出错误
pobj.a()//Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

可取消代理可能的应用场景:在应用中把代理分发到第三方,其中管理你的模型数据,而不是给出真实模型本身的引用。如果你的模型对象改变或被替换,就可以使分发出去的代理失效,这样第三方就可以知晓变化并请求更新到这个模型的引用。

使用代理:

代理在前:

代理与目标交流,首先与代理交互,通过与代理交互来增加某些特殊的规则,这些是message本身没有的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var messages=[],
handlers={
get(target,key){
if(typeof target[key]=='string'){
return target[key].replace(/[^w]/g,"");//过滤标点符号
}
return target[key];//所有其他的传递下去
},
set(target,key,val){
//设定唯一字符串,改为小写
if(typeof val=='string'){//值为字符串,且是唯一值添加元素
if(target.indexOf(val)==-1){
target.push(val.toLowerCase());
}
}
return true;
}
}
var messages_proxy=new Proxy(messages,handlers);
messages_proxy.push('heLLo...',42,"wOrlD!!","WoRld!!");
messages_proxy.forEach(val=>{
console.log(val);
})
messages.forEach(v=>console.log(v));//hello... world!! world!!

代理在后:

让目标与代理交流,而不是代理与目标交流。代理只能与主对象交互,实现方式就是把proxy对象放到主对象的[[Prototype]]链中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var handlers={
get(target,key,context){
return function(){
context.speak(key+"!")
}
}
},
catchall=new Proxy({},handlers),
greeter={
speak(who="someone"){
console.log("hello",who)

}
}
Object.setPrototypeOf(greeter,catchall);//将代理设置为目标对象的__proto___
greeter.speak();
greeter.speak("world");
greeter.everyone();//检查[[Prototype]]链会查看catchall是否有everyone属性,然厚代理的get()处理函数介入并返回一个用访问的属性名("everyone")调用speak()函数

访问不存在的属性名抛出错误

代理在前

1
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
var obj={
a:1,
foo(){
console.log("a:",this.a)
}
},
handlers={
get(target,key,context){
if(Reflect.has(target,key)){
return Reflect.get(target,key,context)
}
else{
throw Error("no such property/method")
}
},
set(target,key,val,context){
if(Reflect.has(target,key)){
return Reflect.set(target,key,val,context)
}else{
throw Error("no such property/method")
}
}
},
pobj=new Proxy(obj,handlers)
pobj.a=3;
pobj.foo();
pobj.b=4;//Uncaught Error: no such property/method

代理在后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var handlers={
get(){
throw "no such property/method"
},
set(){
throw "no such property/method"

}
}
var pobj=new Proxy({},handlers);
var obj={
a:1,
foo(){
console.log("a:",this.a)
}
}
Object.setPrototypeOf(obj,pobj);
obj.a=3;
obj.foo();
obj.b=4;//Uncaught Error: no such property/methodxxc

代理实现多个[[Prototype]]链接

1
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
33
34
35
36
37
38
39
40
var obj1={
name:"obj-1",
foo(){
console.log("obj1.foo",this.name)
}
},obj2={
name:"obj-2",
foo(){
console.log("obj2.foo",this.name)
},
bar(){
console.log("obj2.bar",this.name)
}
},
handlers={
get(target,key,context){
if(Reflect.has(target,key)){
return Reflect.get(target,key,context)
}else{
//伪装多个[[Prototype]]
for(let p of target[Symbol.for("[[Prototype]]")]){
if(Reflect.has(p,key)){
return Reflect.get(p,key,context)
}
}
}
}
},
obj3=new Proxy({
name:"obj3",
baz(){
this.foo();
this.bar()
}
},handlers)
//伪装多个[[Prototype]]的链接
obj3[Symbol.for("[[Prototype]]")]=[obj1,obj2];
obj3.baz();
//obj1.foo:obj-3
//obj2.var:obj-3