【ES6 系列】Proxy 和 Reflect

猪八戒去高老庄找高翠兰,结果高小姐是孙悟空变的,在这个场景中,对于猪八戒来说,孙悟空可以算是高小姐的一个代理,在长相上来说,他们是一致的。猪八戒只能访问到被孙悟空假扮的高小姐,却见不到真正的高小姐。

Proxy

Proxy 基本概念

在上面的场景中,孙悟空就类似于我们今天要讲的 ES6 中的 Proxy,它是一种“代理”,或者可以称之为“拦截”。外界在对一个对象进行访问的时候,都先必须通过这层拦截,才能进行访问。而这个拦截的过程中可以对外界的访问进行过滤和改写。

我们先来看一个例子:

1
2
3
4
5
6
7
8
9
10
let obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});

在上面这个例子中,我们通过 Proxy 对一个空对象进行了拦截,重新定义了对象属性的读取(get)和设置(set)行为。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

1
let proxy = new Proxy(target, handler);

其中,new Proxy()表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。

注意,要使得 Proxy 起作用,必须针对 Proxy 实例(上例是 proxy 对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

如果 handler 没有设置任何拦截,那就等同于直接通向原对象。

1
2
3
4
5
let target = {};
let handler = {};
let proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

常见的 Proxy 拦截操作

Proxy 支持的拦截操作有:

  • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’]。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • 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 实例作为函数调用的操作,比如 proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(…args)。

下面我们就几个常见的拦截进行举例说明。

默认一个对象:

1
2
3
4
5
let obj={
time:'2017-03-11',
name:'net',
_r:123
};

get

1
2
3
4
// 拦截对象属性的读取
get(target,key){
return target[key].replace('2017','2018')
}

set

1
2
3
4
5
6
7
8
// 拦截对象设置属性
set(target,key,value){
if(key==='name'){
return target[key]=value;
}else{
return target[key];
}
}

has

1
2
3
4
5
6
7
8
// 拦截key in object操作
has(target,key){
if(key==='name'){
return target[key]
}else{
return false;
}
}

deleteProperty

1
2
3
4
5
6
7
8
9
// 拦截delete
deleteProperty(target,key){
if(key.indexOf('_')>-1){
delete target[key];
return true;
}else{
return target[key]
}
}

ownKeys

1
2
3
4
// 拦截Object.keys,Object.getOwnPropertySymbols,Object.getOwnPropertyNames
ownKeys(target){
return Object.keys(target).filter(item=>item!='time')
}

Proxy.revocable()

Proxy.revocable 方法返回一个可以取消的 Proxy 实例

1
2
3
4
5
6
7
8
9
10
let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable 方法返回一个对象,该对象的 proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy 实例。上面代码中,当执行 revoke 函数之后,再访问 Proxy 实例,就会抛出一个错误。

Proxy.revocable 的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

Proxy 的 this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理。

1
2
3
4
5
6
7
8
9
10
11
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m() // true

所以在一些情况下,有些对象的属性只能通过正确的 this 才能拿到时,由于 this 指向的变化,导致 Proxy 无法代理目标对象。

1
2
3
4
5
6
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

Reflect

为什么我们要一起来说 Reflect 呢?Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。

设计目的

(1) 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。

(2) 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc)则会返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}

(3) 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name)和 Reflect.deleteProperty(obj, name)让它们变成了函数行为。

1
2
3
4
5
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

(4)Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

1
2
3
4
5
6
7
8
9
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});

相关方法

Reflect 与 ES5 的 Object 有点类似,包含了对象语言内部的方法,Reflect 也有 13 种方法,与 proxy 中的方法一一对应。

Proxy 相当于去修改设置对象的属性行为,而 Reflect 则是获取对象的这些行为。

相关使用大家参照 Object 和 Proxy 的使用即可,不再一一赘述。

Proxy 和 Reflect 实现观察者模式

观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

在我们之前的开发过程中,我们如果想要实现观察者模式的话,我们可能需要进行事件绑定和触发来实现。

1
2
3
Event.listen('changeName', name => console.log(name))

Event.trigger('changeName', name )

但是在 ES6 中,我们可以通过使用 Proxy 和 Reflect 来实现这个目的

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
//添加观察者
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);

//proxy 的set 方法
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
//创建proxy代理
const observable = obj => new Proxy(obj, {set});
//被观察的 对象
const person = observable({
name: '张三',
age: 20
});

function print() {
console.log(`${person.name}, ${person.age}`)
}
function print2() {
console.log(`我是二号观察者:${person.name}, ${person.age}`)
}
//添加观察者
observe(print);
observe(print2);
person.name = '李四';
// 输出
// 李四, 20
// 我是二号观察者:李四, 20

小结

Proxy 和 Reflect 都是 ES6 中针对对象新增的方法,Proxy 修改设置对象的属性行为,而 Reflect 则是获取对象的这些行为。

查看评论