24.JS高级-揭开Proxy与Reflect的神秘面纱

753 阅读32分钟

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

  • 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
  • 课程对照进度:JavaScript高级系列126-131集(coderwhy)
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力

脉络探索

  • 在JavaScript的高级特性中,ProxyReflect是两个强大而神秘的工具,它们赋予了我们前所未有的能力去控制和监听对象的行为。你是否曾经想过,如果能够创建一个对象,它可以在背后默默地监听你对它的每一次操作,那会是什么体验?

  • 本文将带你深入探索ProxyReflect的内在奥秘。你将了解到如何使用Proxy来创建一个代理对象,它可以拦截并自定义对象的操作,比如属性访问、赋值、枚举、删除等。同时,你也会学到Reflect如何作为一个实用工具,与Proxy配合使用,提供了一系列与Object上的方法相对应的反射操作。

  • 但这些特性究竟能做什么?它们在实际开发中有哪些应用场景?又是如何解决一些常见的问题的?让我们带着这些问题,一起揭开它们的神秘面纱

一、监听对象的操作

  • 我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程
    • 通过我们前面所学的知识,能不能做到这一点呢?
    • 其实是可以的,我们可以通过之前的属性描述符中的存储属性描述符defineProperty来做到
    • 在前面章节,有学习到访问器与数据两种不同的属性描述符,而通过其访问器分支get、set,实现监听对象属性的操作
const obj = {
  name:"coderwhy",
  age:35
}

Object.defineProperty(obj,"name",{
  get:function(){
    console.log('监听到该name属性被访问');
  },
  set:function(){
    console.log("监听到该name属性被设置");
  }
})
obj.name//监听到该name属性被访问 
console.log(obj.name);//监听到该name属性被访问  undefined
obj.name = '小余'//监听到该name属性被设置
  • 这里能够看到,不管是查看属性还是设置属性,都能够被get与set所捕获到
    • 但这里有一个问题,那就是监听到属性了,为什么没返回正确的name属性,而是undefined?
    • 这是因为查看属性会触发get,而get没有返回内容,相当于return了一个undefined,关于这一点,我们可以return一个字符串来验证一下,如图24-1,且该描述在MDN文档中也有进行说明

对象的getter与setter说明

图24-1 访问器描述符的get、set说明

//在原来代码基础上,进行get属性描述符变动
  get:function(){
    console.log('监听到该name属性被访问');
    return '参与JS高级共学计划,添加vx:coderwhy666'
  },

get篡改读取内容拦截效果

图24-2 get篡改读取内容拦截效果

  • 通过get监听案例,我们了解到如何监听到对象的操作(查看或者改变),也清楚的知道这是如何返回值的
    • 在get监听案例中,我们返回了一个字符串,而想要返回正确的value值也是简单的,只需要拿到监听的key,然后从对象中针对性获取即可,同时处理下当改变值时,在set中新值覆盖旧值就行
    • 关键在于,该方式的局限性较大,只能监听一个属性,而一个对象中的属性在大多数情况下,都不止一个,此时有什么办法呢?
    • 我们目的从监听对象属性到监听对象的全部属性,首先我们需要先获取全部的key属性,然后遍历key属性填入defineProperty方法中,实现监听全部属性,让我们来尝试一下吧!
const obj = {
  name: "coderwhy",
  age: 35,
  Hobbies:'专研技术'
}

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj,key,{
    get:function() {
      console.log(`监听到${key}属性`);
      return value
    },
    set:function(newValue) {
      console.log(`监听到${key}属性被设置值`);
      value = newValue
    }
  })
})

obj.name//监听到name属性
obj.age//监听到age属性
obj.Hobbies//监听到Hobbies属性

obj.name ='小余'//监听到name属性被设置值
obj.age = 18//监听到age属性被设置值
obj.Hobbies = '看书'//监听到Hobbies属性被设置值

console.log(obj.name);//小余
  • 通过这种方式,解决了单独使用defineProperty方法只能监听单一属性的难点
  • 但是这样做是有缺点的
    • 首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。这种做法很像是利用了该方法的特性,另辟蹊径去实现
    • 在达成监听目的是同时,将初衷原本是定义普通的属性,强行将它变成了数据属性描述符
    • 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty是无能为力的
    • 所以我们要知道,访问器描述符设计的初衷并不是为了去监听一个完整的对象,用在实现监听对象操作上,属于比较勉强,就像不合身的衣服,能穿,但穿着难受不贴合,会妨碍我们的一些行动
  • 在这一方面,有更加合适的API:Proxy,用初衷与用法一致的API,能让我们的意图更加明确可靠

二、Proxy基本使用

  • 在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:

    • 代理,在维基百科上解释为代表授权方处理事务,在这里可以理解为Proxy代表对象处理监听的相关操作
    • 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象),之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作
  • 我们可以将上面的defineProperty案例用Proxy来实现一次(监听obj对象):

    • 首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler
    const p = new Proxy(target, handler)//监听的对象 处理对象
    
    • 其target参数为授权方,也就是要使用 Proxy 包装的目标对象(侦听对象),该目标对象可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
  • 在我们这个案例中,objProxy一开始是没有和obj对象产生联系的,通过target参数,去代理obj对象

    • 此时对obj的所有操作,都应该通过objProxy操作了
    • 我们对objProxy的所有操作,最终效果都会作用在obj对象身上
    • 那多了一层代理,像defineProperty方法完成正常"查询","改动"操作时,我们也能够在这一些关键时机去处理一些事情
const obj = {
  name:"xiaoyu",
  age:20
}

//1.创建一个Proxy对象,Proxy对象是一个类,所以使用new创建
const objProxy = new Proxy(obj,{//代理obj

})

//2.对obj的所有操作,应该去操作objProxy
console.log(objProxy.name);//xiaoyu
objProxy.name = "小余"
console.log(objProxy.name);//小余
console.log(obj);//已经被修改了,{name: '小余', age: 20}
  • 我们之后的操作都是直接对Proxy的操作,而不是原有的对象,而对Proxy的操作,需要使用到第二个参数handler,通常handler被称为处理对象,可以理解为处理Proxy操作的对象,也有handler被译为处理器的原因
    • 在handler中,有众多的捕获器,用来捕捉"查找","改动"这个变化过程的一些时机,不同的捕获器针对于不同的时机和具备不同作用
    • 默认情况下,在我们还不了解有什么捕获器时,我们可以给一个空对象,依旧可以通过操作objProxy代理从而作用于obj对象身上,这并不影响代理的作用,只是没有使用捕获器介入这一过程做出其他事情

API语法参数分类

图24-3 Proxy代理流程

2.1 Proxy的set和get捕获器

  • 我们如果想要介入该操作过程,最主要的是以下两个捕获器:
    1. handler.get:属性读取操作的捕捉器
    2. handler.set:属性设置操作的捕捉器
  • 这两捕获器作用,和一开始我们使用defineProperty的get、set是相同的,在这个基础上,我们可以继续完善Proxy的这个案例代码
    • 这两个捕获器也会接受到对应的参数
    • get:target(监听对象)、key(监听对象的索引)
    • set:target、key、newValue(改动的新值)
  • 通过两个捕获器的这些参数实现"查找"与"改变"操作
    • 在这里能够看到target在模板字符串中,被解析为[object Object],该形式以前有讲解过,会被以toString方法的形式进行解析(当对象需要被转换成字符串形式,如在模板字符串或字符串拼接中使用时,JS 自动调用这个 toString() 方法)
    • 所以如果需要清楚知道target的信息内容,可以单独抽离出来
const obj = {
  name: "coderwhy",
  age: 35,
  Hobbies:'专研技术'
}

const objProxy = new Proxy(obj,{
  get:function(target,key){
    console.log(`监听到${target}对象的${key}属性被访问了`);
    console.log(target,'单独抽离出来');//{ name: '小余', age: 35, Hobbies: '专研技术' } 单独抽离出来
    return target[key]
  },
  set:function(target,key,newValue){
    console.log(`监听到${target}对象的${key}属性被设置值了`);
    target[key] = newValue
  }
})


objProxy.name = '小余'//监听到[object Object]对象的name属性被设置值了
console.log(objProxy.name);//监听到[object Object]对象的name属性被访问了 小余
  • 此时我们再来回顾handler参数:一个对象,其属性是定义代理在对其执行操作时的行为的函数
    • 首先这是一个对象,默认情况下是空对象,在该情况下,对Proxy代理的任何操作都和直接对监听对象的操作没有任何区别
    • 在该对象里面有各种属性,这些属性会在我们对Proxy代理执行操作时,拦截下对应的一些操作
    • handler对象内的属性又称为拦截器,一共13个,都可以算成handler对象的实例方法
    • 每一个handler实例方法都是和正常对象的某个实例方法对应上,从而实现拦截。例如我们实现监听对象的"查找"、"改动",这两个操作,不管是defineProperty还是Proxy的实现方式,都是使用set与get方法,我们可以简单理解为狸猫换太子
  • 最后,我们还要说明set与get分别对应的是函数类型,且刚才故意漏掉这两者都相同的最后参数receiver
  • 该参数指:Proxy 或者继承 Proxy 的对象,具体做什么的,我们暂时跳过,等下再回头来看

2.2 Proxy(handler)的13个捕获器

  • 一个空的处理器(handler)将会创建一个与被代理对象行为几乎完全相同的代理对象。通过在 handler 对象上定义一组函数,我们可以自定义被代理对象的一些特定行为

    • 在这里需要注意,几乎完全相同的代理对象,这意味使用方式和行为是一致的
    • 每个陷阱(捕获器tarp)对应于一个特定的对象操作。如果处理器对象有相应的陷阱方法,则该方法会被调用;如果没有,操作会直接转发给目标对象
    • 陷阱中的逻辑决定了是否将操作重定向至目标对象,是否修改操作的行为,或是否直接返回一个自定义的结果
  • 在正式讲解这13个捕获器之前,了解捕获器是如何生效的更为重要

    1. 拦截机制:JS引擎会在内部为Proxy对象维护一个关联的目标对象和处理器对象。当对Proxy对象进行操作时,这些操作首先被送到处理器对象
    2. 方法查找与执行:对于每种可以拦截的操作,如getsetapply等,处理器对象可以提供一个同名的方法来拦截相应的操作,在处理器对象中查找到对应方法进行执行
  • 在13个捕获器中,有4个常用的,其中两个是已经讲过的set、get,另外两个是has与deleteProperty

    • 这四个陷阱(捕获器)涵盖了对对象进行读取、写入和属性检查的基本操作,这些是日常编程中最常见的操作。几乎所有涉及对象属性的交互都会触及到这些操作,包括访问、修改、检查属性是否存在,以及删除属性


表24-1 捕获器的四个常见方法

陷阱方法对应操作描述
handler.has()in 操作符拦截属性存在性的检查,如 key in proxy
handler.get()属性读取操作拦截对对象属性的读取操作,如 proxy[key]
handler.set()属性设置操作拦截对对象属性的设置操作,如 proxy[key] = value
handler.deleteProperty()delete 操作符拦截对对象属性的删除操作,如 delete proxy[key]
const obj = {
  name:"xiaoyu",
  age:20
}

//1.创建一个Proxy对象,Proxy对象是一个类,所以使用new创建
const objProxy = new Proxy(obj,{//代理obj
  set:function(target,key,value){//target:侦听对象,key:侦听对象的key(你修改那个属性的key),value:修改的新值
    console.log(`监听:监听${key}的设置值`);
    target[key] = value
  },

  get:function(target,key){//obj是会默认传进来参数的
    console.log(`监听:监听${key}的获取`);
    return target[key]
  },

  deleteProperty:function(target,key){//使用上述第11个 delete 操作符的捕捉器
    console.log(`监听:监听删除${key}属性`);//监听:监听删除name属性
    delete target[key]
  },
  has:function(target,key){//使用上述第8个 in 操作符的捕捉器。
    console.log(`监听:监听in判断${key}属性`);
    return key in target//返回结果
  }
})

delete objProxy.name

console.log("age" in objProxy);//通过in 判断"age"有没有再objProxy里面,true
  • 以及如下方剩余的9个捕获器方法,其对应方法来源几乎都来自Object,使用方式一致,因此我们不再进行赘述
    • 在这些方法中,我们同样看到了defineProperty方法,那为什么还要多此一举将set、get单独抽离出来?
    • 在讲解监听对象操作的末尾时,我们说这并不符合defineProperty方法的初衷,因此具备一定的局限性
    • 一个Proxy可以拦截其目标对象上的所有getset操作,而不仅仅是单个属性。非常适合于创建一个全面拦截和操作对象访问行为的模型,而我们通过defineProperty方法时,还需要forEach遍历一下,手动进行一些较为复杂的操作,需要对每个属性和对象重复定义
    • 而且Proxy可以根据属性名、目标对象的状态或其他外部条件动态地改变属性的行为。这提供了比Object.defineProperty()更大的灵活性


表24-2 handler剩余九个捕获器总结

陷阱方法对应操作描述
handler.apply()函数调用拦截函数调用操作,如 proxy(...args)
handler.construct()new 操作符拦截构造函数调用操作,如 new proxy(...args)
handler.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor()拦截获取对象属性的描述符操作
handler.defineProperty()Object.defineProperty()拦截定义或修改属性描述符的操作
handler.getPrototypeOf()Object.getPrototypeOf()拦截获取对象原型的操作
handler.setPrototypeOf()Object.setPrototypeOf()拦截设置对象原型的操作
handler.isExtensible()Object.isExtensible()拦截检查对象是否可扩展的操作
handler.preventExtensions()Object.preventExtensions()拦截阻止对象扩展的操作
handler.ownKeys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()拦截获取对象自身属性的键的操作,包括非继承的属性和符号属性
  • Object.defineProperty()的初衷是在对象初始化时用于设定属性的特殊行为,一旦初始化结束后,就不再频繁变动(固定下来,除非再次使用defineProperty进行修改,JS也不希望轻易进行变动),属于是静态的行为,更适用于那些对象结构已知且不需要动态改变访问行为的情况
    • 而监听对象,一有变化立刻行动,是属于动态调整的范畴,需要随时准备拦截对象的操作,Proxy在这方面更具备优势,可以根据条件动态地修改拦截行为,无需重新定义属性或对象,能够应对复杂的或动态变化的应用场景
    • 在执行方面,由于Proxy 的设计和实现是作为 ECMAScript 语言标准的一部分,JS引擎会专门进行优化(如内联缓存),具备更独特的优势,而Proxy作为代理,用途比defineProperty更加广泛,毕竟defineProperty方法在handler中也只是13个拦截器之一
    • 况且如果在一个对象上频繁使用 Object.defineProperty(),尤其是在其原型链上,可能会导致性能下降,因为每次属性访问都可能需要解析更复杂的定义和条件
  • 一旦打算使用defineProperty方法来实现该监听操作,所监听对象的性质就必须被迫变为访问属性描述符,哪怕原本是数据属性描述符也会被迫转变,这是不合理的

三、Reflect的作用

  • Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射,通常配合Proxy进行使用
    • 需要注意Reflect不是类,也不是构造函数或者函数对象,而是一个标准的内置对象,所以我们可以直接Reflect.xxx的进行使用,而不能通过new调用
    • Reflect中的所有方法都是静态方法,就像Math对象一样
  • 那么这个Reflect有什么用呢?
    • 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法
    • 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf()
    • 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()
  • 这里我们能够看到,怎么连方法名都一样了呢?如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
    • 这是因为在早期的ECMA规范中没有考虑到这种对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面
    • 但后续Object上的新东西越来越多,Object越来越重,对于最顶层的Object来说,身为所有类的父类,他本身不应该包含太多的东西的,因为父类里的东西是会被继承到子类中的,太多的东西必然会加重子类的负担而过于臃肿
    • 且Object作为一个构造函数,这些语言内部操作(即元编程操作)的方法操作实际上放到它身上并不合适,另外还包含一些类似于 in、delete操作符,让JS对象看起来是会有一些奇怪的
    • 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect内置对象上,这和Proxy中的handler是对应起来的,一模一样的13个方法,有些方法是Reflect新增的,并非全部来自Object对象

API语法参数分类

图24-4 Proxy/Handler与Reflect的方法对应

  • 通过MDN文档所对应的关系,能够看到其关系紧密相连,并且在该文档中,也有详细对比了Object和Reflect
    • Reflect 的方法通常返回更标准化的结果。在成功时,许多 Reflect 的操作会返回操作的结果(例如返回 true 或者属性值),在失败时返回 false,而不是抛出异常。这和 Object 的某些方法(如 Object.defineProperty)在遇到错误时抛出异常的行为不同。这种设计在面对错误处理更加一致和可控
    • Reflect内的方法作为和Math一样的静态方法,它的方法不会被任何对象继承。这种设计避免了在对象原型链中可能出现的混乱和冗余,确保了 Reflect 的方法仅用于反射和底层操作,而不会被意外地用于业务逻辑或其他目的
    • Reflect和Proxy进行配合使用也非常的顺手,一一对应的关系,从使用角度上看,非常契合,因为在一开始设计的时候,这两者就注定相辅相成
  • 和Proxy进行对应,我们简单练习一下最常见的set、get、has、deleteProperty这四个方法,并进行表格总结
// 定义一个简单的对象
const obj = {
    name: "coderwhy",
    age: 30
};

// 使用 Reflect.has() 检查对象中是否存在指定的属性
console.log("检查 'name' 是否存在:", Reflect.has(obj, 'name'));  // 输出 true
console.log("检查 'gender' 是否存在:", Reflect.has(obj, 'gender'));  // 输出 false

// 使用 Reflect.get() 获取对象属性的值
console.log("Name:", Reflect.get(obj, 'name'));  // 输出 "coderwhy"
console.log("Age:", Reflect.get(obj, 'age'));  // 输出 30

// 如果属性不存在,可以提供一个默认值
console.log("Gender:", Reflect.get(obj, 'gender', 'Not specified'));  // 输出 "Not specified"

// 使用 Reflect.set() 设置对象属性的值
Reflect.set(obj, 'age', 31);  // 设置 age 属性为 31
console.log("Updated Age:", obj.age);  // 输出 31

// 使用 Reflect.deleteProperty() 删除对象的一个属性
Reflect.deleteProperty(obj, 'name');  // 删除 name 属性
console.log("Name after deletion:", obj.name);  // 输出 undefined

// 再次使用 Reflect.has() 检查 name 属性是否还存在
console.log("检查 'name' 删除后是否存在:", Reflect.has(obj, 'name'));  // 输出 false


表24-3 Reflect的常见方法

方法参数描述返回值
Reflect.has()target, propertyKey判断一个对象是否存在某个属性,等同于 in 运算符Boolean:如果属性存在则返回 true,否则返回 false
Reflect.get()target, propertyKey, [receiver]获取对象身上的某个属性值,类似于 target[name]属性的值;如果属性不存在,则返回 undefined
Reflect.set()target, propertyKey, value, [receiver]设置对象某个属性的值Boolean:如果设置成功则返回 true,否则返回 false
Reflect.deleteProperty()target, propertyKey删除对象的一个属性,相当于 delete target[name]Boolean:如果删除成功则返回 true,否则返回 false

3.1 Reflect的使用

  • 那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
    • 修改成Reflect进行操作肯定是有好处,例如返回值失败情况下明确false而非抛出异常,这是更可预测的错误处理方式,也不需要使用try-catch来捕获错误,更加动态灵活,更加函数式编程(Reflect方法全是函数)
    • 且Reflect的主要应用场景也是配合Proxy进行处理,但其他Object中的相同方法,也可以用Reflect进行取代使用
const obj = {
  name:"小余",
  age:20
}

const objProxy = new Proxy(obj,{
  set:function(target,key,newValue,receiver){
    //下面这种写法好不好,规范吗? 有点奇怪,因为直接操作原对象了
    // target[key] = value
    //代理对象的目的:不再直接操作原对象,所以我们采用间接操作的方式(好处一)
    //从语言层面通过反射去操作
    const isSuccess = Reflect.set(target,key,newValue)
    //Reflect.set会返回布尔值,可以判断本次操作是否成功(好处二)
    if(!isSuccess){
      throw new Error(`set${key}failure`)
    }
  },
  get:function(target,key,receiver){

  }
})

//操作代理对象
objProxy.name = "xiaoyu"
console.log(obj);//{name: 'xiaoyu', age: 20},修改成功

3.2 Reflect其余方法(9个)

  • 而Reflect剩余的9个方法,使用方式是曾经所学习过,因此只会简单进行说明,不是我们的重点,有用到再来翻阅即可

  • Reflect.getPrototypeOf(target)

    • 类似于 Object.getPrototypeOf()。

    • 用于获取一个对象的原型(也称为 prototype)。

      该方法返回该对象的原型对象,即该对象继承的对象。如果该对象没有继承任何对象,则返回 null

  • Reflect.setPrototypeOf(target, prototype)

    • 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true
  • Reflect.isExtensible(target)

    • 类似于 Object.isExtensible()
    • 用于判断一个对象是否可以被扩展,即是否可以添加新的属性
    • 该方法返回一个布尔值,表示该对象是否可以被扩展,即是否可以通过 Object.defineProperty() 或者直接赋值添加新的属性
  • Reflect.preventExtensions(target)

    • 类似于 Object.preventExtensions()。返回一个Boolean

    • 用于阻止一个对象被扩展,即不允许添加新的属性

      该方法返回一个布尔值,表示该对象是否被阻止了扩展,即是否不允许添加新的属性

      其中,target 是要阻止扩展的对象。

  • Reflect.getOwnPropertyDescriptor(target, propertyKey)

    • 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined
  • Reflect.defineProperty(target, propertyKey, attributes)

    • 和 Object.defineProperty() 类似。如果设置成功就会返回 true
  • Reflect.ownKeys(target)

    • 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响)
  • Reflect.apply(target, thisArgument, argumentsList)

    • 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply() 功能类似
  • Reflect.construct(target, argumentsList[, newTarget])

    • 对构造函数进行 new 操作,相当于执行 new target(...args)

3.3 Proxy与Reflect中的receiver参数

  • receiver参数较难理解,是位于Proxy、Reflect这两者的get与set方法中的最后一个参数,那么它的作用是什么?
    • 如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this
  • 在正式讲解前,我们需要了解getter与setter正确的使用逻辑,如图
const obj = {
  _name: 'coderwhy',
  get name() {
    return this._name
  },
  set name(newValue) {
    this._name = newValue
  }
}

obj.name = '小余'
console.log(obj.name);

对象的setter、getter的监听层效果

图24-5 对象的setter、getter的监听层效果

  • 此时加上我们的Proxy代理层和对应的get、set捕获器,以及对应的Reflect的set、get方法来实现
    • 相对于一开始的最初数据源、监听层、实际使用,多了一层代理层(Proxy)
    • 而这有什么区别,我们再来画一个图看一下,如图
const obj = {
  //最初数据源
  _name: 'coderwhy',
  //监听层
  get name() {
    return this._name
  },
  set name(newValue) {
    this._name = newValue
  }
}

//代理层
const objProxy =new Proxy(obj,{
  get:function(target,key){
    return Reflect.get(target,key)
  },
  set:function(target,key,newValue){
    Reflect.set(target,key,newValue)
  }
})

//实际使用
objProxy.name = '小余'
console.log(objProxy.name);//小余

API语法参数分类

图24-6 Proxy代理层拦截效果

  • 在这里能够看到,Reflect借用Proxy拦截下实际使用监听层的时机,进行真正的处理
    • 查询获取顺序:实际使用->Proxy代理层get拦截->Reflect.get触发->监听层getter触发->从数据源中获取到数据->返回查询结果
    • 改动顺序:实际使用->Proxy代理层set拦截->Reflect.set触发->监听层setter触发->修改数据源
  • 在这里能够看到,Proxyget 方法和obj 对象的 name 属性的 getter 方法都会触发
    • 而这个过程的Reflect方法只是搭上顺风车,在Proxy的内部进行操作
    • 在这个过程中,get、set的触发顺序分别为Proxy、Reflect、监听层setter
  • 那Reflect.set和get不会和obj中的setter和getter产生冲突吗?这就需要理解它们是如何相互作用的
    • 首先是不会冲突,这是一个协作的过程
    • Reflect.set 本质上是在“请求”对属性的设置,如果属性有 setter,它就会触发这个 setter。因此,如果在 setter 中有额外的逻辑处理或者修改值,那么最终的属性值会是 setter 执行后的结果。在这个过程中,Reflect.set 只是作为触发器
    • Reflect.get也是如此,本质上是在“请求”获取属性值,如果属性有 getter,它就会触发这个 getter。因此,返回的值将是 getter 执行后的结果,包括任何逻辑处理或值的修改
  • Reflect身上的get与set会尊重对象身上的getter和setter,最终的决定权依旧在setter和getter身上,但如果我们已经在Reflect中进行操作,也就没有继续操作getter和setter的动机
  • 此时有一个问题,obj对象中的getter与setter此时内部的this._name指向的是哪一个对象,是obj对象还是objProxy对象呢?
    • obj对象中的name方法作为普通函数而非箭头函数,其this是会受到影响的
    • 那此时的this对象指向的是谁?答案是obj对象,而这又是为什么呢?
    • 根据我们刚才的查找顺序改动顺序,能够确定数据的最终处理权依旧在obj对象身上,此时obj与objProxy代理对象的关系并不够紧密
  • this指向于obj,而非objProxy代理,该情况下,在处理继承或原型链时,可能会导致 this 指向问题
const obj = {
  _name: 'coderwhy',
  get name() {
    return this._name
  },
  set name(newValue) {
    this._name = newValue
  }
}

const objProxy =new Proxy(obj,{
  get:function(target,key){
    console.log('被访问:',target,key);
    return Reflect.get(target,key)
  },
  set:function(target,key,newValue){
    console.log('被设置:',target,key);
    Reflect.set(target,key,newValue)
  }
})
//这在Reflect.set触发之前打印的,所以输出的_name为未修改状态,在浏览器控制台则为最终结果'小余'
objProxy.name = '小余'//被设置: { _name: 'coderwhy', name: [Getter/Setter] } name
console.log(objProxy.name);//被访问: { _name: '小余', name: [Getter/Setter] } name   小余
  • 如果我们使用了Proxy代理,我们肯定是希望代理更加完善的,尤其是要用Proxy进行包装的目标对象范围是任何类型的对象,包括原生数组,函数,甚至另一个代理。而对象的背后存在着继承等因素,此时就需要this层面的拦截

    • 这就必须要说到我们的Receiver参数的(Proxy、Reflect的get、set最后参数)

    • Receiver参数类似各种数组方法中的最后参数thisArg

    • 不同之处在于,Proxy和Reflect的Receiver参数需要结合起来,我们拿这两者的get方法举例

    1. Proxy.get方法的Receiver参数是:Proxy自身代理
    2. Reflect.get方法是Receiver参数是:如果target对象中指定了getterreceiver则为getter调用时的this
  • 这就可以结合起来了,Proxy.get的Receiver参数提供确定的this值,Reflect.get的Receiver参数提供this放置的位置

    • 在这方面上,get与set是一样的,此时,我们再来进行刚才的set设置,等实践完成,我们再来进行说明
const obj = {
  _name: 'coderwhy',
  get name() {
    return this._name
  },
  set name(newValue) {
    this._name = newValue
  }
}

const objProxy =new Proxy(obj,{
  get:function(target,key,receiver){
    console.log('被访问:',target,key);
    return Reflect.get(target,key,receiver)
  },
  set:function(target,key,newValue,receiver){
    console.log('被设置:',target,key);
    Reflect.set(target,key,newValue,receiver)
  }
})
//这在Reflect.set触发之前打印的,所以输出的_name为未修改状态,在浏览器控制台则为最终结果'小余'
objProxy.name = '小余'
// 被设置: { _name: 'coderwhy', name: [Getter/Setter] } name
// 被设置: { _name: 'coderwhy', name: [Getter/Setter] } _name
console.log(objProxy.name);
// 被访问: { _name: '小余', name: [Getter/Setter] } name
// 被访问: { _name: '小余', name: [Getter/Setter] } _name
  • 可以看到在加上receiver之后,objProxy代理的get、set方法都被调用了两次
    • 在这两次结果中,通过key,我们能察觉到有所不同,一个是name,一个是_name
    • 在第一次拦截中,是正常在Proxy调用了Reflect的set与get,这是与监听层形成交互,此时的key是指obj中的name方法(setter、getter)

Proxy代理层调用Reflect与监听层形参交互

图24-7 Proxy代理层调用Reflect与监听层形参交互

  • 主要在第二次拦截中,监听层的this被Reflect的receiver所改变,变为Proxy代理本身,此时在obj中的代码就会变为如下形式
const obj = {
  _name: 'coderwhy',
  get name() {
    return objProxy._name//{ _name: 'coderwhy', name: [Getter/Setter] }._name
  },
  set name(newValue) {
    objProxy._name = newValue
  }
}
  • 而objProxy的内容是: { _name: '小余', name: [Getter/Setter] }
    • 此时访问的则是里面的_name,这时候我们再来回头看打印内容,就会发现一目了然
    • 两次输出,意味着objProxy在两个不同的地方被调用了,一次在Proxy代理层,一次在监听层
    • Proxy和Reflect的receiver做到了替换掉obj对象中的this,从而进一步提高拦截的完整度
objProxy.name = '小余'
// 被设置: { _name: 'coderwhy', name: [Getter/Setter] } name
// 被设置: { _name: 'coderwhy', name: [Getter/Setter] } _name
console.log(objProxy.name);
// 被访问: { _name: '小余', name: [Getter/Setter] } name
// 被访问: { _name: '小余', name: [Getter/Setter] } _name

Proxy与Reflect中的receiver配合作用

图24-8 Proxy与Reflect中的receiver配合作用

  • 最后,我们来总结一下receiver
    • Reflect中的receiver更加重要,是改变this的核心
    • 而Proxy中的receiver虽然与Reflect更搭,但值不一定就必须使用Proxy代理对象,而是根据自己实际需求决定
  • 在这里,我们能够看到,Proxy的receiver通常表示Proxy本身,那为什么不直接使用Proxy,而是还专门设计一个receiver出来呢?
    • 这和this的原因很像,Proxy所返回的代理是固定的,例如我们的objProxy,虽然在大多数情况下可能是期望的行为,但这已经是限制死了,并不是动态决定,this 绑定总是指向 objProxy,一旦涉及继承或多层代理,就可能会出现问题。所以在MDN文档中的描述中,receiver的值除了本身之外,还包括了继承 Proxy 的对象,从这点也说明了其动态性,直接写死(固定)并不是一个好的选择
    • 当代理对象继承自另一个对象时,通过 receiver 传递正确的 this 可以确保在整个原型链中方法和访问器属性的调用上下文正确。这确保方法或访问器在访问 this 时能够访问到正确的属性,而不是错误地访问到代理对象或基对象的属性


表24-4 Proxy与Reflect中的receiver对比

特性Proxy get / set 中的 receiverReflect get / set 中的 receiver
作用用于在 get/set 中处理属性时传递正确的 this 绑定,通常是代理对象用于将 this 绑定传递到目标对象的 getter/setter,确保操作时正确绑定上下文
默认值当调用 Proxygetset 时,receiver 通常是代理对象(即 Proxy 本身)需要显式传递,通常为调用时的对象,比如代理对象或者目标对象
用途确保 getter/setter 中的 this 指向代理对象或自定义的对象,而非目标对象允许将 this 绑定为代理对象或自定义对象,特别是在代理对象操作中,这能避免循环引用或错误行为
对于 getter 的影响receiver 确保 getter 中的 this 被正确绑定,尤其在继承链或代理链中传递正确的上下文如果不传递 receiverReflect.get 默认将 this 绑定为目标对象,可能导致不正确的上下文
对于 setter 的影响receiver 确保 setter 中的 this 被正确绑定,确保属性更新在代理对象上发生如果 receiver 是代理对象,Reflect.set 将确保 setter 中的 this 绑定为代理对象,而非目标对象
示例场景用于确保代理对象在代理链中的正确 this 绑定,防止直接操作目标对象用于处理复杂的代理和继承关系,传递自定义的上下文,确保操作时 this 指向正确的对象

3.4 Reflect中的construct方法

  • Reflect.construct() 方法的行为有点像 new 操作符构造函数,相当于运行 new target(...args)
    • 该语法的形式在第六章节函数一等公民末尾曾经介绍过,这里就不过多赘述
//target:被运行的目标构造函数
//argumentsList:类数组,目标构造函数调用时的参数
//newTarget:作为新创建对象的原型对象的 constructor 属性
Reflect.construct(target, argumentsList[, newTarget])//有返回值
  • 想要知道construct方法的作用,我们需要举一个应用场景来说明
    • 在下方案例中,Student是一个构造函数,通过Student所new出来的对象,自然是Student类型
    • 现在有一个需求,我希望new Student,结果是Teacher类型
  • 在以前是很难做到的,需要进行额外的操作,例如使用工厂函数
function Student(name,age){
  this.name = name
  this.age = age
}

function Teacher(name,age){

}

const stu = new Student('小余',20)
console.log(stu);//Student { name: '小余', age: 20 }
console.log(stu.__proto__ === Student.prototype);//true
  • 但Reflect.construct方法可以实现该操作,只需要一行代码即可实现
    • 参数1是我们的目标对象,参数2是原先目标对象内的参数数据,参数3是要改变为的类型
    • 并且能够发现teacher的隐式原型等于Teacher的显示原型,而这意味着,该类型并不是简单的改变
    • 构造函数和原型的分离,意味着任何在 Teacher.prototype 上定义的方法或属性都可以被 teacher 对象访问
    • 造就了一个使用 Student 的构造逻辑和 Teacher 的原型,这是非常灵活的继承类型,打破了传统构造函数和原型继承的限制
  • 但同时我们也应该清楚,越是灵活,就越是双刃剑,在一般情况下,我们是用不到该方法的
const teacher = Reflect.construct(Student,['coderwhy',35],Teacher)
console.log(teacher);//Teacher { name: 'coderwhy', age: 35 }
console.log(teacher.__proto__ === Teacher.prototype);//true
  • 在Babel源码的ES6转ES5的继承中,就使用了该方式
    • 该函数主要用来生成一个“超类”构造函数,也就是用于在派生类中调用基类(超类)的构造函数,通常是在派生类的构造函数中通过 super() 实现的
    • 而在该源码中,不允许使用super去调用父类的构造函数(逻辑数据),因为在其他地方做出限制,使用super会报错
    • 此时就通过Reflect.construct方法,将super目标(父类)作为目标对象,以新创建的当前构造函数进行继承,实现了当前构造函数的原型是自身,而内在构造逻辑是super目标(父类),另类的实现了和super调用一样的效果
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal(){
    var Super = _getPrototypeOf(Derived),
    result;
    if(hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      //Reflect体现,NewTarget为接下来要使用的构造函数类型,借用了父类的构造逻辑,形成了更加灵活的result初始结果
      result = Reflect.construct(Super,arguments,NewTarget);
    }else{
      result = Super.apply(this,arguments)
    }
    return _possibleConstructorReturn(this,result)
  }
}

后续预告

  • 在下一章节,我们会综合这几篇所学内容来实现一个响应式案例,这是各类应用性前端框架的核心原理之一,掌握了响应式,在学习Vue或者React中,将会如虎添翼,对数据的流动与改变,将会得心应手
    • 框架会迭代,响应式会不断优化,但主要的枝干从不轻易改变,这是一个值得去深入掌握的思想原理!