Reflect

Posted by Youzi Blog on December 19, 2019

Reflect 语法及使用

Reflect是 ES6 中用来操作对象的 API;

在 ES6 之前我们只能使用Object.method来访问原生对象的内部方法,ES6 语法将大部分内部方法加到了Reflect上,所以我们现在可以使用Reflect.defineProperty这样的形式来调用内部方法;

这个方法还修改了一些内部方法的返回值,使用Object.defineProperty来定义一个无法配置的属性时,会抛出错误,而调用Reflect.defineProperty会返回false

ES5 使用in | delete保留字来判断或者删除属性,这样的写法很 JS 但很不 OO,过于命令式,在 ES6 后可以使用Reflect.has | Reflect.deleteProperty来实现,这很coooooooool

Reflect对象的方法可以在Proxy对象的方法中使用,Proxy对象总共有 13 种代理方法,在Reflect中都有对应的方法;我们在代理方法中可以直接调用Reflect.method来调用默认的原生对象方法,完成默认的对象操作,再添加自己的操作;

ProxyReflect对象一起使用时,为了避免重复递归触发当前代理方法,调用Reflect方法不会再触发当前的方法;比如在handler里设置了set代理,那在set代理中再调用Reflect.set方法不会再次触发代理的set方法;

由于ReflectProxy对象的方法都一样的,参数也差不多,这里就不介绍太多,罗列一下方法名和参数即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
Reflect.get(target, name, receiver);
Reflect.set(target, name, value, receiver);
Reflect.has(target, name);
Reflect.apply(target, thisArg, args);
Reflect.ownKeys(target);
Reflect.construct(target, args);
Reflect.isExtensible(target);
Reflect.defineProperty(target, name, desc);
Reflect.deleteProperty(target, name);
Reflect.getPrototypeOf(target);
Reflect.setPrototypeOf(target, prototype);
Reflect.preventExtensions(target);
Reflect.getOwnPropertyDescriptor(target, name);

参数中target必须是一个对象,否则会直接报错;receiver表示,当读取的属性部署了get | set函数,函数的this绑定到receiver

实例

Reflect.get(target, name, receiver)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  }
};
var myReceiverObject = {
  foo: 4,
  bar: 4
};
Reflect.get(myObject, 'baz', myReceiverObject); // 8

let p = new Proxy(
  {},
  {
    get(t, k, r) {
      console.log(t, k, r);
      return Reflect.get(t, k, r) + 1;
    }
  }
);

从上述代码可以看出,在get函数中,this被改写成myReceiverObject了,这就是第三个参数的作用了;

Reflect.set(target, name, value, receiver)

get方法类似,多传入了指定的属性值;当和Proxy对象一起使用时,在Proxy方法内部使用了set方法并传入了第四个参数receiver,会额外触发Proxy.defineProperty方法,看一个例子;

1
2
3
4
5
6
7
8
9
10
11
12
13
let o = {};
let p = new Proxy(o, {
  set(t, k, v, r) {
    console.log('set');
    return Reflect.set(t, k, v, r);
  },
  defineProperty(t, k, p) {
    console.log('define');
  }
});
p.a = 10;
// set
// define

这是因为传入了receiver参数改变了set函数内部this的指向,上述代码指向了Proxy实例p,所以其实是在给实例p做定义属性操作,此时就会触发defineProperty的代理;如果不传入第四个参数,这时就是直接对第一个参数target指向的o对象操作了,就不会触发代理了;

Reflect.has(obj, name)

该方法对应 ES5 的in运算符,在Proxy那篇也提到过,in运算符不区分是否是在原型上的属性,这个方法也一样,我们可以用这个方法来判断属性是否存在对象上(不论是在对象本身还是在对象的原型上);

1
2
3
4
5
6
7
8
9
10
11
let o = {
  a: 1
};
let p = Object.create(o);
p.b = 2;
// ES5
'a' in p;
'b' in p;
// ES6
Reflect.has(p, 'a');
Reflect.has(p, 'b');

Reflect.deleteProperty(obj, name)

用于删除对象的方法;返回布尔值,删除成功或要删除的属性不存在,都返回true;删除失败,属性依然存在返回false

1
2
3
4
5
let o = { a: 1 };
// ES5
delete o.a;
// ES6
Reflect.deleteProperty(o, 'a');

Reflect.construct(target, args)

等同于new Class(),是创建构造函数的方法;

1
2
3
4
5
6
7
8
function Person(name, age) {
  this.name = name;
  this.age = age;
}
// ES5
let p = new Person('steven', 24);
// ES6
let q = Reflect.construct(Person, ['stevent', 24]);

Reflect.getPrototypeOf(obj)

用于读取对象的__proto__属性,对应Object.getPrototypeof方法;毕竟浏览器环境才有可能配置对象的原型属性;另外用Object.getPrototypeof方法会将参数转化成对象,但是Reflect.getPrototypeOf不会强制转换,会先进行类型检测,非对象类型直接抛出异常。

Reflect.setPrototypeOf(obj, newProto)

用于设置原型,返回布尔值,表示是否设置成功;如果无法设置目标对象的原型,就会返回false

Reflect.apply(func, thisArg, args)

用于绑定this对象后执行第一个给定的参数;我们平常使用apply | call方法来绑定函数的this,现在我们可以使用这个方法来替代了;一个比较直观的例子:

1
2
3
4
5
6
7
8
let a = [1, 2, 3];
// ES5
let min = Math.min.apply(Math, a);
let max = Math.max.apply(Math, a);
let type = Object.prototype.toString.call(a);
// ES6
let min1 = Reflect.apply(Math.min, null, a);
let type1 = Reflect.apply(Object.prototype.toString, a, []);

那按照 ES6 的写法,我们可以封装一个APPLY函数,用于改变函数内部this值并执行;

1
2
3
function APPLY(method, _this, ...args) {
  return Reflect.apply(method, _this, args);
}

Reflect.defineProperty(target, propertyKey, attributes)

用来显式定义对象属性,等同于Object.defineProperty

Proxy.defineProperty方法共用,例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
let p = new Proxy(
  {},
  {
    defineProperty(t, k, d) {
      console.log(t, k, d);
      return Reflect.defineProperty(t, k, d);
    }
  }
);
p.a = 10;
// {} "a" {value: 10, writable: true, enumerable: true, configurable: true}
p.a;
// 10

Reflect.getOwnPropertyDescriptor(target, propertyKey)

用于获取对象属性的描述对象;几乎和Object.getOwnPropertyDescriptor功能一致,除了Reflect会先检测第一个参数的类型外;

Reflect.isExtensible(target)

用于判断对象是否可扩展;返回布尔值;和Object.isExtensible功能一致;

Reflect.preventExtensions(target)

用于让一个对象变为不可扩展的对象;返回布尔值,表示是否操作成功;与Object.preventExtensions功能一致;

Reflect.ownKeys(target)

用于返回对象自身所有属性,包括属性名是Symbol类型的,不包括原型上的属性;基本等同于Object.getOwnPropertyNames + Object.getOwnPropertySymbols的并集;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4
};

// 旧写法
Object.getOwnPropertyNames(myObject);
// ['foo', 'bar']

Object.getOwnPropertySymbols(myObject);
//[Symbol(baz), Symbol(bing)]

// 新写法
Reflect.ownKeys(myObject);
// ['foo', 'bar', Symbol(baz), Symbol(bing)]

使用 Proxy 实现观察者模式

defineProperty 使用中,写了一个使用Object.definePropery方法实现观察者模式 demo 的代码片段;这一小节我们会使用Proxy来实现观察者模式;

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
 * 发布者的构造函数
 * @author: youzi
 */
class Publisher {
  /**
   * 发布者的构造函数
   * @param {object} data 需要绑定发布和订阅的对象
   * @author: youzi
   */
  constructor(data) {
    this.data = data;
    this.watcherList = {};
    // this.pushWatcherList();
    this.watchData();
  }
  /**
   * 添加观察者实例至观察者列表中
   * @param {Watcher} watcher 观察者实例
   * @author: youzi
   */
  pushWatcherList(watcher) {
    let key = watcher.key;
    // let fun = watcher.method;
    if (!this.watcherList[key]) {
      this.watcherList[key] = [];
    }
    this.watcherList[key].push(watcher);
  }
  /**
   * 创建Proxy实例,目标对象是data
   * @return {Proxy} Proxy实例
   * @author: youzi
   */
  watchData() {
    const that = this;
    const handler = {
      set(t, k, v, r) {
        // 默认的set行为
        const result = Reflect.set(t, k, v, r);
        // 触发观察者列表中的方法
        that.watcherList[k].map(watcher => {
          watcher.method(k, v);
        });
        return result;
      }
    };
    this.$data = new Proxy(this.data, handler);
  }
  /**
   * 代替原生的set方法,规定只能使用这种方法来set对象的某个值
   * @param {string} key 对象的key
   * @param {any} val 对象的value
   * @author: youzi
   */
  setData(key, val) {
    this.$data[key] = val;
  }
}

/**
 * 订阅者的构造函数
 * @author: youzi
 */
class Observer {
  /**
   * 订阅者的构造函数
   * @param {string} key 订阅对象的key
   * @param {function} method 发布者发布消息后,订阅者的响应函数
   * @param {Publisher} publisher 绑定的发布者实例
   * @author: youzi
   */
  constructor(key, method, publisher) {
    this.key = key;
    this.method = method;
    this.publisher = publisher;
    publisher.pushWatcherList(this);
  }
}

const data = {
  a: 1,
  b: 2,
  c: {
    d: 3
  }
};
let p = new Publisher(data);
const fun = (k, v) => {
  console.log(k, v);
};
let w1 = new Observer('a', fun, p);
let w11 = new Observer('a', fun, p);
let w2 = new Observer('d', fun, p);
p.setData('a', 10);
p.setData('d', 12);

总的来说,观察者模式如果使用Proxy来实现,我们需要返回的是Proxy实例,并且只有对实例操作才会使得set代理函数生效;上面的代码中,我们需要显示调用对象赋值方法setData才会触发代理,直接对data进行操作是无效的;

总结

Reflect对象作用就是用来代替Object的某些方法,所以都在ES5中有源函数可以查到的;另外在Proxy代理函数中用到的很多,因为这俩对象的方法都是一一对应的,参数也一样,我们一般不会去直接替代对象的原生方法,而是选择在原生方法的基础上添加自己写的方法,所以这时使用Reflect就很方便了。