天天看点

JS中的代理与反射 — Proxy和Reflect

一提到ES6,相信大家不会陌生。自2015年正式发布至今一直备受追捧。它所带来的很多特性提高了日常开发效率,改变了构建代码的思维方式。作为一个规范,起到了推动语言发展的作用。

最近在和同组同学聊天的时候,偶然得知他们准备使用proxy来解决目前框架的一些不足。突然发现Proxy是一个不太容易被人提起的东西。在好多ES6语法用的非常熟的情况下,依然会有一些东西被忽略掉。例如接下来要提到的Proxy和Reflect。

Proxy和Reflect是什么

Proxy正常翻译做代理,可以作用与对象或者函数上,数据ES6的新特性之一,用以实现js的元编程。

const proxy = new Proxy();           

使用Proxy,可以对函数或者对象的一些操作进行“代理”,或者这个地方叫做拦截会更合适一些。也就是说,使用代理,可以拦截对象或者函数的一些操作,这些操作包括但不限于:读、写、in等等操作。

有一个使用的场景:如何能够打日志记录下来某一个类的实例被读取过什么字段以及什么字段被赋值过?

我们先来看Proxy的构造函数

<T extends object>(target: T, handler: ProxyHandler<T>): T           

从函数定义可以看出,Proxy在构建的时候,需要传入被代理的对象target以及一个handler。

被代理的对象可以是一个空对象{},那handler是什么呢?

handler实际上是一个对象,其记录了需要代理的行为,以及代理的方法。一个典型的handler定义如下

const handler = {
  get(target, propKey, receiver) {
    console.log('GET '+ propKey);
       // do other thing...
  },
  set(target, propKey, value, receiver) {
       console.log('SET ' + propKey);
       // do other thing...
   },
}           

使用这个代理就能够在对象被读取或者被写入的情况下,打印出具体被操作的key。

接下来我们做一个实验。假设我想把上面handler的规则,运用到某个对象上。

const obj = {};
const objP = new Proxy(obj, handler);
objP.hello = 'hello';           

控制台会打印出下面的内容

SET hello
"hello"           

然而假设这个时候,你直接在控制台中输入objP.hello,并期望得到objP.hello的值hello的时候,你得到的只有

GET hello
undefined           

而并没有"hello"。这是因为上面的handler对get和set进行了拦截,并没有去执行真正的get、set的代码。所以变量没有被赋值。而这个时候就需要Reflect出场了。

Reflect是ES6的另一个新特性,主要用户对象上,实现了JS的反射

看到这里,会JAVA的同学要开心了:反射?我熟啊!!

恩,这里的反射和JAVA的理解差不多。可以把对象的结构及其他相关信息“反射出来”。通过这个新对象,可以方便的获取到对象的结构、props、参数函数等等,并且可以以更优雅的方式来调用props里的方法而避免一些问题的发生。

回到上面那个例子,需要把handler给改一下:

const handler = {
  get(target, propKey, receiver) {
    console.log('GET '+ propKey);
       // do other thing...
    return Reflect.get(target, propKey, receiver);
  },
  set(target, propKey, value, receiver) {
       console.log('SET ' + propKey);
       // do other thing...
    return Reflect.set(target, propKey, value, receiver);
   },
}           

与上面例子不同的是,在get和set的最后,加上了Reflect.get、Reflect.set,使用这样的方式,能够让get、set继续执行它之前应该执行的行为。

再执行一遍上面的实例,最后在敲入objP.hello的时候,就能得到想要的结果。

GET hello
"hello"           

使用场景

说了这么多用法,举几个可以使用的场景。

1 通过对set、get函数的代理,我们可以对访问不存在的对象属性进行单独的处理。

get(target, propKey, receiver) {
    console.log('GET '+ propKey);
    if(!(propKey in target)){
      throw new Exception('null target');
    }
    return Reflect.get(target, propKey, receiver);
  },           

2 MVVM的数据双绑。

function observedArray(cb) {
    const array = [];
    return new Proxy(array, {
        set(target, propertyKey, value, receiver) {
            cb(propertyKey, value);
            return Reflect.set(target, propertyKey, value, receiver);
        }
    });    
}
const array = observedArray(
    (key, value) => console.log(`${key}: ${value}`));

array.push('a');           

这个时候当array数组元素改变的时候,会调用对应的回调。

3 做

Type checking

Proxy支持的代理方法列表

以下列表摘选自

EXPLORING ES6

对象类:

  • defineProperty(target, propKey, propDesc) : boolean           
  • deleteProperty(target, propKey) : boolean           
  • get(target, propKey, receiver) : any           
  • getOwnPropertyDescriptor(target, propKey) : PropDesc|Undefined           
  • getPrototypeOf(target) : Object|Null           
  • has(target, propKey) : boolean           
  • isExtensible(target) : boolean           
  • ownKeys(target) : Array<PropertyKey>           
  • preventExtensions(target) : boolean           
  • set(target, propKey, value, receiver) : boolean           
  • setPrototypeOf(target, proto) : boolean           
    函数类
  • apply(target, thisArgument, argumentsList) : any