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

目录
  • 引言
  • 前置知识
  • 单独使用 Proxy
  • Proxy 中的 receiver
  • Reflect 中的 receiver
  • 总结
  • 结尾

引言

EcmaScript 2015 中引入了 Proxy 代理Reflect 反射 两个新的内置模块。

我们可以利用 Proxy 和 Reflect 来实现对于对象的代理劫持操作,类似于 Es 5 中 Object.defineProperty()的效果,不过 Reflect & Proxy 远远比它强大。

大多数开发者都了解这两个 Es6 中的新增内置模块,可是你也许并不清楚为什么 Proxy 一定要配合 Reflect 使用。

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

前置知识

  • Proxy 代理,它内置了一系列”陷阱“用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
  • Reflect 反射,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy的方法相同。

简单来说,我们可以通过 Proxy 创建对于原始对象的代理对象,从而在代理对象中使用 Reflect 达到对于 JavaScript 原始操作的拦截。

如果你还不了解 & ,那么赶快去 MDN 上去补习他们的知识吧。

毕竟大名鼎鼎的 VueJs/Core 中核心的响应式模块就是基于这两个 Api 来实现的。

单独使用 Proxy

开始的第一个例子,我们先单独使用 Proxy 来烹饪一道简单的开胃小菜:

const obj = {
  name: 'wang.haoyu',
};

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

proxy.name // 劫持你的数据访问name -> wang.haoyu

看起来很简单对吧,我们通过 Proxy 创建了一个基于 obj 对象的代理,同时在 Proxy 中声明了一个 get 陷阱。

当访问我们访问 proxy.name 时实际触发了对应的 get 陷阱,它会执行 get 陷阱中的逻辑,同时会执行对应陷阱中的逻辑,最终返回对应的 target[key] 也就是所谓的 wang.haoyu .

Proxy 中的 receiver

上边的 Demo 中一切都看起来顺风顺水没错吧,细心的同学在阅读 Proxy 的 MDN 文档上可能会发现其实 Proxy 中 get 陷阱中还会存在一个额外的参数 receiver 。

那么这里的 receiver 究竟表示什么意思呢?大多数同学会将它理解成为代理对象,但这是不全面的。

接下来同样让我们以一个简单的例子来作为切入点:

const obj = {
  name: 'wang.haoyu',
};

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 '19Qingfeng';
  },
};

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

const obj = {
  name: 'wang.haoyu',
};

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

// log: false
obj.value

关于原型上出现的 get/set 属性访问器的“屏蔽”效果,我在这篇文章中进行了详细阐述。这里我就不展开讲解了。

我们可以看到,上述的代码同样我在 proxy 对象的 get 陷阱上打印了 console.log(receiver === proxy); 结果却是 false 。

那么你可以稍微思考下这里的 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 指向的是 obj 。

当然,你不要将 revceiver 和 get 陷阱中的 this 弄混了,陷阱中的 this 关键字表示的是代理的 handler 对象。

比如:

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

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: 'wang.haoyu',
};

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

// log: false
obj.value

Reflect 中的 receiver

在清楚了 Proxy 中 get 陷阱的 receiver 后,趁热打铁我们来聊聊 Reflect 反射 API 中 get 陷阱的 receiver。

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

const parent = {
  name: '19Qingfeng',
  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: 'wang.haoyu',
};

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

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

我们稍微分析下上边的代码:

  • 当我们调用 obj.value 时,由于 obj 本身不存在 value 属性。
  • 它继承的 proxy 对象中存在 value 的属性访问操作符,所以会发生屏蔽效果。
  • 此时会触发 proxy 上的 get value() 属性访问操作。
  • 同时由于访问了 proxy 上的 value 属性访问器,所以此时会触发 get 陷阱。
  • 进入陷阱时,target 为源对象也就是 parent ,key 为 value 。
  • 陷阱中返回 Reflect.get(target,key) 相当于 target[key]
  • 此时,不知不觉中 this 指向在 get 陷阱中被偷偷修改掉了!!
  • 原本调用方的 obj 在陷阱中被修改成为了对应的 target 也就是 parent 。
  • 自然而然打印出了对应的 parent[value] 也就是 19Qingfeng 。

这显然不是我们期望的结果,当我访问 obj.value 时,我希望应该正确输出对应的自身上的 name 属性也就是所谓的 obj.value => wang.haoyu 。

那么,Relfect 中 get 陷阱的 receiver 就大显神通了。

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

const handler = {
  get(target, key, receiver) {
-   return Reflect.get(target, key);
+   return Reflect.get(target, key, receiver);
  },
};

const proxy = new Proxy(parent, handler);

const obj = {
  name: 'wang.haoyu',
};

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

// log: wang.haoyu
console.log(obj.value);

上述代码原理其实非常简单:

  • 首先,之前我们提到过在 Proxy 中 get 陷阱的 receiver 不仅仅会表示代理对象本身同时也还有可能表示继承于代理对象的对象,具体需要区别与调用方。这里显然它是指向继承与代理对象的 obj 。
  • 其次,我们在 Reflect 中 get 陷阱中第三个参数传递了 Proxy 中的 receiver 也就是 obj 作为形参,它会修改调用时的 this 指向。

你可以简单的将 Reflect.get(target, key, receiver) 理解成为 target[key].call(receiver),不过这是一段伪代码,但是这样你可能更好理解。

相信看到这里你已经明白 Relfect 中的 receiver 代表的含义是什么了,没错它正是可以修改属性访问中的 this 指向为传入的 receiver 对象。

总结

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

我们再来稍稍回忆一下,针对于 get 陷阱(当然 set 其他之类涉及到 receiver 的陷阱同理):

  • Proxy 中接受的 Receiver 形参表示代理对象本身或者继承与代理对象的对象。
  • Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。

结尾

这里就到了文章的结尾了,至于为什么会突然提到 Proxy & Reflect 的话题。

其实是笔者最近在阅读 Vue/corejs 的源代码内容,刚好它内部大量应用于 Proxy & Reflect 所以就产生了这篇文章。

关于 Proxy 为什么一定要配合 Reflect 使用,具体结合 VueJs 中响应式模块的依赖收集其实会更好理解一些。不过这里为了照顾不太熟悉 VueJs 的同学所以就没有展开了。

当然,最近我也在阅读 VueJs 的过程中尝试书写一些阶段性总结文章。之后在文章中也会详细讲解这一过程,有兴趣的同学可以持续关注我的最新动态~

到此这篇关于js中为什么Proxy一定要配合Reflect使用的文章就介绍到这了,更多相关js Proxy Reflect内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 理解JavaScript中的Proxy 与 Reflection API

    一.创建 Proxy let target = {} let proxy = new Proxy(target, {}) proxy.name = "proxy" console.log(proxy.name) // proxy console.log(target.name) // proxy target.name = "target" console.log(proxy.name) // target console.log(target.name) // t

  • js中为什么Proxy一定要配合Reflect使用

    目录 引言 前置知识 单独使用 Proxy Proxy 中的 receiver Reflect 中的 receiver 总结 结尾 引言 EcmaScript 2015 中引入了 Proxy 代理 与 Reflect 反射 两个新的内置模块. 我们可以利用 Proxy 和 Reflect 来实现对于对象的代理劫持操作,类似于 Es 5 中 Object.defineProperty()的效果,不过 Reflect & Proxy 远远比它强大. 大多数开发者都了解这两个 Es6 中的新增内置模块

  • vue.config.js中devServer.proxy配置说明及配置正确不生效问题解决

    目录 proxy 配置确认.解析 原因 一 原因二 注意 详解 pathRewrite changeOrigin 总结 proxy 配置确认.解析 devServer: { https: true, // 开启https模式 // 配置服务器代理 proxy: { '/api': { // 匹配访问路径中含有 '/api' 的路径 target: 'http://localhost:3000/', // 测试地址.目标地址 changeOrigin: true, ws: true, // 是否开

  • JavaScript中的Proxy对象

    Js中Proxy对象 Proxy对象用于定义基本操作的自定义行为,例如属性查找.赋值.枚举.函数调用等. 语法 const proxy = new Proxy(target, handler); target: 要使用Proxy包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理. handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理proxy的行为. 描述 Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,

  • JS中showModalDialog关闭子窗口刷新主窗口用法详解

    本文实例讲述了JS中showModalDialog关闭子窗口刷新主窗口用法.分享给大家供大家参考,具体如下: 网上找了好长时间 大都是window.opener.location.reload(),等等 都不是我想要的 最后终于发现了一个 想知道的就往下看看吧 showModalDialog和showModelessDialog 一.showModalDialog和showModelessDialog有什么不同? showModalDialog:被打开后就会始终保持输入焦点.除非对话框被关闭,否

  • 详解Node.js中的事件机制

    前言 在前端编程中,事件的应用十分广泛,DOM上的各种事件.在Ajax大规模应用之后,异步请求更得到广泛的认同,而Ajax亦是基于事件机制的. 通常js给我们的第一印象就是运行在客户端浏览器上面的脚本,通过node.js我们可以在服务端运行javascript. node.js是基于单线程无阻塞异步式的I/O,异步式的I/O指的是当遇到I/O操作的时候,线程不阻塞而是进行下面的操作,那么I/O操作完成之后,线程时如何知道该操作完成的呢? 当操作完成耗时的I/O操作之后,会以事件的形式通知I/O操

  • 详解设计模式中的proxy代理模式及在Java程序中的实现

    一.代理模式定义 给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象. 著名的代理模式的例子就是引用计数(reference counting): 当需要一个复杂对象的多份副本时, 代理模式可以结合享元模式以减少存储器的用量.典型做法是创建一个复杂对象以及多个代理者, 每个代理者会引用到原本的对象.而作用在代理者的运算会转送到原本对象.一旦所有的代理者都不存在时, 复杂对象会被移除. 要理解代理模式很简单,其实生活当中就存在代理

  • 浅谈js中几种实用的跨域方法原理详解

    这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据.只要协议.域名.端口有任何一个不同,都被当作是不同的域. 下表给出了相对http://store.company.com/dir/page.html同源检测的结果: 要解决跨域的问题,我们可以使用以下几种方法: 一.通过jsonp跨域 在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的.但是,在页面上引入不同

  • JS中多步骤多分步的StepJump组件实例详解

    最近的工作在做一个多步骤多分步的表单页面,这个多步骤多分步的意思是说这个页面的业务是分多个步骤完成的,每个步骤可能又分多个小步骤来处理,大步骤之间,以及小步骤之间都是一种顺序发生的业务关系.起初以为这种功能很好做,就跟tab页的实现原理差不多,真做下来才发现,这里面的相关逻辑还是挺多的(有可能是我没想到更好地办法~),尤其是当这个功能跟表单,还有业务数据的状态结合起来的时候.我把这个功能相关的一些逻辑抽象成了一个组件StepJump,这个组件能够实现纯静态的分步切换和跳转,以及跟业务相结合的复杂

  • Node.js中npm常用命令大全

    npm是什么 NPM的全称是Node Package Manager,是随同NodeJS一起安装的包管理和分发工具,它很方便让JavaScript开发者下载.安装.上传以及管理已经安装的包. npm install 安装模块 基础语法 npm install (with no args, in package dir) npm install [<@scope>/]<name> npm install [<@scope>/]<name>@<tag>

  • 总结js中的一些兼容性易错的问题

    一.属性相关 我们通常把特征(attribute)和属性(property)统称为属性,但是他们确实是不同的概念, 特征(attribute)会表现在HTML文本中,对特征的修改一定会表现在元素的outerHTML中,并且特征只存在于元素节点中: 属性(property)是对于JS对象进行修改,除了浏览器内置的部分特征外,其它的属性操作并不会影响HTML文本. 1. IE6/7不区分属性和特征 其它浏览器会区分属性和特征,而IE67并不会区分它们,在IE67下我们只能用属性名来删除特征,虽然这两

随机推荐