为什么 Proxy 一定要配合 Reflect 使用?

目录

前言

前置知识

单独使用 Proxy

Proxy 中的 receiver

Reflect 中的 receiver

总结


前言

EcmaScript 2015 中引入了 Proxy 和 Reflect 两个新的内置模块。

大家可能或多或少都了解过 Proxy 和 Reflect,因为vue3就是使用 Proxy 和 Reflect 实现响应式的,但是大家可能不清楚为什么 Proxy 一定要配合 Reflcet 使用。下面举几个例子来描述一下他们之间的关系。

前置知识

Proxy: 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。

它不直接操作对象,而是代理模式,通过代理对象进行操作,然后在进行操作的同时,可以添加一些额外操作。

Reflect:它提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的方法相同。

简单来说,我们可以通过 Proxy 创建代理对象,从而在代理对象中使用 Reflect 达到对于 JavaScript 原始操作的拦截。如果你还不了解,赶紧点击 此链接 去补习吧。

单独使用 Proxy

请看下面代码:

let cat = {
  name: "Tom",
  age: 2,
};
let proxy = new Proxy(cat, {
  get(target, key, receiver) {
    console.log("正在读取 %s 属性", key);
    return target[key];
  },
});
proxy.name; // 正在读取 name 属性
proxy.age; // 正在读取 age 属性

我们通过 Proxy 创建了一个基于 target 对象的代理对象,同时声明了一个 get 方法,我们访问 proxy.name 时实际触发了 get 方法,最后返回 target[key],也就是 "Tom"。

Proxy 中的 receiver

细心的同学可能已经发现上面代码中 get 方法中的 receiver 参数没有使用,那么这个参数究竟代表什么意思呢?大家可能会把他理解成代理对象的实例。

现在请看下面代码:

let target = {
  name: "Tom",
};
let proxy = new Proxy(target, {
  get(target, key, receiver) {
    console.log(receiver === proxy);
    return target[key];
  },
});
proxy.name; // true

上面代码中,我们在 get 方法中打印了 receiver 严格等于 proxy。控制台输出 true。表示这里的 receiver 的确是和代理对象 proxy 是相等的。

所以 receiver 是可以表示代理对象的实例,但是这仅仅是 receiver 代表的一种情况而已。

请看下面代码:

let parent = {
  get value() {
    return "parent";
  },
};

let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    console.log(receiver === proxy);
    return target[key];
  },
});

let child = { name: "Jerry" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);

child.value; // false

 上面代码同样在 get 方法中打印了 receiver 严格等于 proxy,控制台输出结果却是 false。

这究竟是为什么呢?其实这就是 receiver 存在的意义。

他是为了传递正确的调用者指向,请看下面代码:

let parent = {
  get value() {
    return "parent";
  },
};

let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    console.log(receiver === proxy);
    console.log(receiver === child);
    return target[key];
  },
});

let child = { name: "Jerry" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);

child.value; // false  true

上面代码,get 方法中多打印了一个 receiver 严格等于 child,控制台打印一次为 fasle 、 true。

我们可以清楚的看到 receiver 代表的是 继承了 代理对象 proxy 的 child

到这里,我们明白了 Proxy 中 get 方法的 receiver 参数不仅仅代表 Proxy 的实例,某种情况下,他也会代表继承 Proxy 的那个对象。

Reflect 中的 receiver

我们了解了 Proxy 中的 receiver 参数,代表的是 Proxy 实例或者继承 proxy 的那个对象。

请看代码:

let parent = {
  name: "Tom",
  get value() {
    return this.name;
  },
};

let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    return Reflect.get(target, key);
  },
});

let child = { name: "小Tom" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);

console.log(child.value);

打印 child.value 控制台输出的是 "Tom"。

我们分析下上面代码。

当我们调用 child.value 的时候,child 本身并不存在 value 属性。

但是它继承的 proxy 对象中存在 value 属性的访问方法。

所以触发 proxy 上的 get value(),同时由于访问了 proxy 上的 value属性,所以触发 proxy 的 get 方法。

get 方法的 target 参数就是 parent,key 参数 就是 value。

然后方法中 return Reflect.get(target, key) 相当于 target[key]。

此时,我们访问的 child 对象的 value 属性,return 的却是 parent 的 value 属性。

不知不觉中 this 指向在 get 方法中被偷偷修改了,原本调用的 child 在 get 方法中 变成了 parent。

所以打印出来的是 parent[value],也就是 "Tom"。

这显然不是我们期望的结果,当我们访问 child.value 时,我们希望输出 child 对象的 name 属性,也就是 "小Tom"。

那么,Reflect 中 get 方法的 receiver 参数该上场了。

let parent = {
  name: "Tom",
  get value() {
    return this.name;
  },
};

let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});

let child = { name: "小Tom" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);

console.log(child.value); // 小Tom

上面的代码和前面的一样,只是把 get 方法中 returnReflect.get(target, key) 修改成了 

return Reflect.get(target, key, receiver) 之后,打印出来的就是我们期望的结果了。

原理很简单:

首先,我们之前提到过 Proxy 中 get 方法的 receiver 参数不仅仅表示代理对象本身,同时也有可能是继承了代理对象的对象,具体区别于调用方。

这里显然他是指向继承了 proxy 的 child

然后,我们在 Reflect 中的 get 方法第三个参数传入了 Proxy 中的 receiver,也就相当于 child 作为形参,它会修改调用时的 this 指向。

Reflect 中的 receiver 参数可以把属性访问中的 this 指向 receiver 对象。

总结

看到这里,相信大家都已经明白了,为什么 Proxy 一定要配合 Reflect 使用。就是为了触发代理对象拦截操作时,保持正确的 this 指向。

Proxy 中接受的 receiver 形参,表示代理对象本身 或者 继承了代理对象的对象。

Reflect 中传入的 recriver 实参,表示修改执行原始操作时的 this 指向。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值