Proxy搭配Reflect,两者之间产生的化学反应可太好玩了!

1,547 阅读5分钟

在上一篇文章讲到了 Proxy 类的使用,那么在本篇内容中将会讲解 Reflect 对象,Proxy 可以捕获13种不同的基本操作,这些基本操作有各自不同的反射 Reflect API 方法、参数、关联 ECMAScript 操作和不变式。

Reflect 对象是一个平凡对象,例如 Math,不像其他内置原生值一样是函数或者构造器,它特有对应于各自可控的元数据任务的静态函数。这些函数一对一对应着代理可以定义的处理函数方法,具体如下图所示:

image.png

一般来说这些方法和 Object.* 对应的方法行为类似,但是有一个区别是如果第一个参数(目标对象)不是对象的话,它会试图把他类型转换为一个对象,而这种情况下 Reflect.* 方法会抛出一个错误。

Reflect出现的背景

在前面的内容中讲到了 Proxy 的使用,就在来回顾一下,如下代码所示:

const moment = {
  address: "广州",
};

const proxy = new Proxy(moment, {
  get: function (target, key) {
    console.log(target === moment); // true
    return target[key];
  },
});

console.log(proxy.address); // 广州

在上面的代码中,使用 Proxy 对接收到的 target 进行操作,实际上还是操控的是原来的 moment 对象,那么 Reflect 就很好的解决了这个问题,按照个人理解,Proxy 结合 Reflect 的流程图如下图所示:

image.png

在下文会讲到为什么会是这个流程图。

Reflect的基本使用

Reflect 的方法有很多,这里就不一一列举了,讲两三个就好了。

get()

get(...) 捕获器会在获取属性值的操作中被调用,对应的 Reflect 方法为 Reflect.get(),该方法接收三个参数,它们分别是:

  • target: 目标对象;
  • property: 引用的目标对象上的字符串属性;
  • receiver: 代理对象或继承代理对象的对象;

具体实例代码如下所示:

const moment = {
  address: "广州",
};

const proxy = new Proxy(moment, {
  get: function (target, key) {
    return Reflect.get(target, key);
  },
});

console.log(proxy.address); // 广州

set()

set() 捕获器会在设置属性值的操作中被调用,对应的 Reflect 方法为 Reflect.set(),它接收四个参数,它们分别是:

  • target: 目标对象;
  • property: 引用的目标对象上的字符串属性;
  • value: 要赋给属性的值;
  • receiver: 代理对象或继承代理对象的对象;

具体实例代码如下所示:

const moment = {
  address: "广州",
};

const proxy = new Proxy(moment, {
  get: function (target, key) {
    return Reflect.get(target, key);
  },
  set: function (target, key, value) {
    const result = Reflect.set(target, key, value);
    console.log(result); // true
  },
});

proxy.address = "肇庆";

如果返回 true 则表示成功,返回 false 则表示失败,阳模式下回抛出 TypeError

receiver的妙用

在前面的内容中,我们可以看出,使用 Reflect.set() 方法中,它会有一个返回值,成功或者失败,而使用 Proxy 则并没有,这也就导致了我们无法得知是否修改成功了,它不仅有这个作用,还有一个很重要的作用,那就是 get()set() 方法中的最后一个参数了。

先来举一个没有使用 receiver 的例子,如下代码所示:

let index = 0;

const moment = {
  _nickname: "wao",

  set nickname(value) {
    this._nickname = value;
  },
  get nickname() {
    console.log(++index, "moment对象"); // 1 proxy
    return this._nickname;
  },
};

const proxy = new Proxy(moment, {
  get: function (target, key) {
    console.log(++index, "proxy"); // 2 moment对象
    return Reflect.get(target, key);
  },
  set: function (target, key, value) {
    Reflect.set(target, key, value);
  },
});

console.log(proxy.nickname);

这段代码的主要流程主要是经过了以下的步骤,具体如下图所示:

image.png

这些代码经过了这些步骤之后,最终把 wao 值返回,通过查看 index 的输出值也可以查看证实这一论证。在这个时候,我们这个时候的 this 指向的 moment 对象,它已经绕过了 Proxy 直接访问原对象了。

我们要想改变它的 this 指向,那么我们可以使用最后一个参数 receiver,我们再来看看这个参数是什么来的,如下图所示:

image.png

这个 receiver 参数正是发挥 Reflect 对象的关键作用了,当我们传这个参数到 Reflect 的时候,moment 对象的 this 也从 moment 改变成了 proxy 了:

image.png

修改一下代码,你会发现,proxy 对象中的 get() 方法被调用了两次:

const moment = {
  _nickname: "wao",

  get nickname() {
    return this._nickname;
  },
};

const proxy = new Proxy(moment, {
  get: function (target, key, receiver) {
    console.log(`访问了 ${key} 属性`);
    return Reflect.get(target, key, receiver);
  },
});

proxy.nickname;

具体的输出如下图所示:

image.png

这也验证了我们内容开头中所讲到的流程图。

construct()

construct() 捕获器会在 new 操作符中被嗲欧勇,对应的 Reflect 方法为 Reflect.construct(),该方法接收三个参数,它们分别是:

  • taret: 目标构造函数;
  • argumentList: 传给目标构造函数的参数列表;
  • newTarget:最初被调用的构造函数;

具体实例代码如下所示:

function moment() {}

const proxy = new Proxy(moment, {
  construct: function (target, args, newTarget) {
    console.log(target); // [Function: moment]
    console.log(args); // 7
    console.log(newTarget); // [Function: moment]

    return Reflect.construct(...arguments);
  },
});

new proxy(7);

数据绑定和可观察对象

通过 Proxy 可以把运行时钟原本不相关的部分联系到一起,这样就可以实现各种模式,从而让不同的代码相互先操作,例如可以把集合绑定到一个事件派发程序,每次插入新实例时都会发送消息:

const UserList = [];

function emit(value) {
  console.log(value);
}

const proxy = new Proxy(UserList, {
  set(target, key, value, receive) {
    const result = Reflect.set(...arguments);

    if (result) emit(Reflect.get(target, key, receive));

    return result;
  },
});

proxy.push("moment"); // moment

proxy.push("nickname"); // nickname

最后完结撒花,Reflect 的元数据能力提供了模拟各种语法特性的编程等价物,把之前隐藏的抽象数据暴露出来。

解决 Proxy 无法解决的问题

在上一篇文章中,我们的 Proxy 对象存在一些问题,这个时候我们的 Reflect 就可以对这些问题很好地解决了:

const target = new Date();

const proxy = new Proxy(target, {});

console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: this is not a Date object.

// 加入了 Reflect 之后
const target = new Date();

const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    if (typeof target[prop] === 'function') {
      return function(...args) {
        return target[prop].apply(target, args);
      };
    }
    return Reflect.get(target, prop, receiver);
  }
});

console.log(proxy instanceof Date); // true
console.log(proxy.getDate()); // 正常输出当前日期的日部分,不会抛出错误

参考文献

  • JavaScript高级程序设计;

总结

这篇文章讲完,将会讲解 Typescript 中的 Reflect Metadata

你要忍,忍到春暖花开;你要走,走到灯火通明;你要看过世界辽阔,再评判是好是坏,你要卯足劲变好,再旗鼓相当站在不敢想象的人身边; 你要变成想象中的样子,这件事,一步都不能让。

本文正在参加「金石计划」