React 前端导航

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

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

引言

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

利用 Proxy 和 Reflect 我们可以实现对象的代理劫持,类似于 ES5 的 Object.defineProperty,不过 Reflect 和 Proxy 功能更加强大。

很多人应该都了解 Proxy 和 Reflect,但是你可能不知道为什么 Proxy 一定要配合 Reflect 使用。

这里,文章通过几个通俗易懂的例子来讲述它们之间相辅相成的关系。

Proxy 和 Reflect

  • Proxy 内置了一系列方法用于创建对象的代理,实现基本操作的拦截和自定义(例如属性赋值、属性枚举、调用函数、属性读取等等)。

  • Reflect 提供了拦截 js 操作的方法,这些方法与 Proxy 的方法一样。

简单的说就是通过 Proxy 创建原始对象的代理对象,在代理对象中使用 Reflect 达到对 js 原始操作的拦截。

如果你还不了解 Proxy 和 Reflect,你可以去 MDN 上去看看相关的介绍。

如何使用 Proxy?

我们先单独使用 Proxy:

const obj = {
  name: 'demoName',
};

const proxy = new Proxy(obj, {
  // get方法中target表示原对象 key表示访问的属性名
  get(target, key) {
    console.log('劫持你的属性访问' + key);
    return target[key];
  },
});

proxy.name // 劫持你的属性访问name -> demoName

例子很简单,通过 Proxy 我们创建了 obj 对象的代理对象,同时在 Proxy 中声明了一个 get 方法。

当访问 proxy.name 的时候实际触发了对应的 get 方法,执行 get 方法中的逻辑,返回对应的 target[key], 也就是 demoName.

Proxy 中的 receiver 表示什么?

细心的同学在阅读 Proxy 的 MDN 文档时会发现 Proxy 的 get 方法有第三个 receiver 参数 。

那么 receiver 表示的是什么呢?很多人将它理解成代理对象,但这是不全面的。

接下来我们以一个例子来说明:

const obj = {
  name: 'demoName',
};

const proxy = new Proxy(obj, {
  // get方法中target表示原对象 key表示访问的属性名
  get(target, key, receiver) {
    console.log(receiver === proxy);
    return target[key];
  },
});

// log: true
proxy.name;

上面的代码中,我们在 Proxy 对象的 get 方法上接收了 receiver 参数。

同时在方法内部 增加了日志 console.log(receiver === proxy); 它会打印出 true ,表示 receiver 的确是与代理对象相等。

因此我们可以得出结论, receiver 的确是可以表示代理对象,但是这仅仅是 receiver 代表的一种情况,还有另外一种情况,我们接着往下看。

我们来看另外的一个例子:

const parent = {
  get value() {
    return 'farmerLZJ';
  },
};

const proxy = new Proxy(parent, {
  // get方法中target表示原对象 key表示访问的属性名
  get(target, key, receiver) {
    console.log(receiver === proxy);
    return target[key];
  },
});

const obj = {
  name: 'demoName',
};

// 设置obj继承parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
obj.value

上面的代码在 proxy 对象的 get 方法里打印了 console.log(receiver === proxy); 结果却是 false 。
s
我们可以思考下这里的 receiver 是什么?这也是 proxy 中 get 方法第三个 receiver 存在的意义:即为了传递正确的调用者指向

我们来看看下面的代码:

...
const proxy = new Proxy(parent, {
  // get方法中target表示原对象 key表示访问的属性名
  get(target, key, receiver) {
-   console.log(receiver === proxy) // log:false
+   console.log(receiver === obj) // log:true
    return target[key];
  },
});
...

简单的说,get 方法中的 receiver 的存在就是为了正确的在方法中传递上下文。

在属性访问时,get 方法还会触发对应的属性访问器,也就是 get 访问器方法。

可以看到上述的 receiver 代表的是继承 Proxy 的对象 obj。

因此,我们可以得出结论: Proxy 的 get 方法中 receiver 可能是 Proxy 代理对象本身,也可能是继承 Proxy 的那个对象。

本质上来说 receiver 的存在就是为了确保方法函数中调用者的正确的上下文访问,比如这里的 receiver 代表的就是 obj 对象。

这里容易存在一个误区,就是不要将 receiver 和 get 方法中的 this 弄混了,方法中的 this 关键字表示的是代理的 handler 对象。

比如:

const parent = {
  get value() {
    return 'farmerLZJ';
  },
};

const handler = {
  get(target, key, receiver) {
    console.log(this === handler); // log: true
    console.log(receiver === obj); // log: true
    return target[key];
  },
};

const proxy = new Proxy(parent, handler);

const obj = {
  name: 'demoName',
};

// 设置obj继承parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
obj.value

Reflect 中的 receiver

了解了 Proxy 中 get 方法的 receiver 之后,我们再看看 Reflect 中 get 方法的 receiver。

在 Proxy 中(以下都以 get 方法为例)第三个参数 receiver 代表的是代理对象本身或者继承代理对象的对象,它表示触发方法时正确的上下文。

const parent = {
  name: 'farmerLZJ',
  get value() {
    return this.name;
  },
};

const handler = {
  get(target, key, receiver) {
    return Reflect.get(target, key);
    // 这里相当于 return target[key]
  },
};

const proxy = new Proxy(parent, handler);

const obj = {
  name: 'demoName',
};

// 设置obj继承parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
console.log(obj.value);

我们分析下上面代码的执行逻辑:

  • 当调用 obj.value 时,obj 本身不存在 value 属性。

  • obj 继承的 proxy 对象中存在 value 的属性访问操作符,所以会发生屏蔽效果。

  • 此时会触发 proxy 上的 get value() 属性访问操作。

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

  • 进入方法时,target 为源对象也就是 parent ,key 为 value 。

  • get 方法中返回 Reflect.get(target,key) 就等同于 target[key]。

  • 此时, this 的指向在 get 方法中被偷偷修改掉了!!

  • 原本调用方的 obj 在方法中被修改成了对应的 target, 也就是 parent 。

  • 因此打印出了对应的 parent[value],也就是 farmerLZJ 。

很明显,这不是我们期望的结果,当访问 obj.value 时,我们期望输出的时候对应的自身上的 name 属性,也就是 obj.value => demoName 。

这种场景下,Reflect 中的 receiver 就能发挥作用了。

const parent = {
  name: 'farmerLZJ',
  get value() {
    return this.name;
  },
};

const handler = {
  get(target, key, receiver) {
    //return Reflect.get(target, key);
    return Reflect.get(target, key, receiver);//这里增加了receiver参数
  },
};

const proxy = new Proxy(parent, handler);

const obj = {
  name: 'demoName',
};

// 设置obj继承parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: demoName
console.log(obj.value);

上面代码的原理非常简单:

  • 首先,在 Proxy 中 get 方法的 receiver 可能表示代理对象本身或者表示继承代理对象的对象,具体表示需要区别调用方。这里显然它是指向继承代理对象的 obj 。

  • 其次,在 Reflect 的 get 方法中第三个参数我们传递了 Proxy 中的 receiver, 也就是 obj 作为形参,它会修改调用时的 this 指向。

我们可以将 Reflect.get(target, key, receiver) 理解成为 target[key].call(receiver),不过这是一段伪代码,但是这样描述可能让你更容易理解。

总结

看到这里,相信大家都已经理解了为什么Proxy一定要配合Reflect使用,是为了触发代理对象的劫持时保证正确的 this 上下文指向。

我们回忆一下,针对 get 方法(set 方法或其他涉及到 receiver 的方法同理):

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

  • Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。

声明:本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。邮箱:farmerlzj@163.com。 本站原创内容未经允许不得转载,或转载时需注明出处: 内容转载自: React前端网:https://qianduan.shop/blogs/detail/70
想做或者在做副业的朋友欢迎加微信交流:farmerlzj,公众号:生财空间站。

#js#proxy#reflect

相关推荐

原型与原型链、继承

原型与原型链、继承简单实现

浏览器中的js事件循环(Event loop)

本文将简述浏览器中的js事件循环机制,帮助我们理解浏览器环境js代码是如何运行的。Javascript的一大特点是单线程,也就意味着同一时间他只能做一件事。事件循环(Event Loop)是为了协调事件,用户交互,UI渲染,网络处理等行为,防止线程阻塞而诞生的。