该系列文章连载于公众号coderwhy和掘金XiaoYu2002中
- 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
- 课程对照进度:JavaScript高级系列126-131集(coderwhy)
- 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力
脉络探索
-
在JavaScript的高级特性中,
Proxy
和Reflect
是两个强大而神秘的工具,它们赋予了我们前所未有的能力去控制和监听对象的行为。你是否曾经想过,如果能够创建一个对象,它可以在背后默默地监听你对它的每一次操作,那会是什么体验? -
本文将带你深入探索
Proxy
和Reflect
的内在奥秘。你将了解到如何使用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文档中也有进行说明
图24-1 访问器描述符的get、set说明
//在原来代码基础上,进行get属性描述符变动
get:function(){
console.log('监听到该name属性被访问');
return '参与JS高级共学计划,添加vx:coderwhy666'
},
图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对象身上,这并不影响代理的作用,只是没有使用捕获器介入这一过程做出其他事情
图24-3 Proxy代理流程
2.1 Proxy的set和get捕获器
- 我们如果想要介入该操作过程,最主要的是以下两个捕获器:
handler.get
:属性读取操作的捕捉器handler.set
:属性设置操作的捕捉器
- 这两捕获器作用,和一开始我们使用defineProperty的get、set是相同的,在这个基础上,我们可以继续完善Proxy的这个案例代码
- 这两个捕获器也会接受到对应的参数
- get:target(监听对象)、key(监听对象的索引)
- set:target、key、newValue(改动的新值)
- 通过两个捕获器的这些参数实现"查找"与"改变"操作
- 在这里能够看到target在模板字符串中,被解析为
[object Object]
,该形式以前有讲解过,会被以toString方法的形式进行解析(当对象需要被转换成字符串形式,如在模板字符串或字符串拼接中使用时,JS 自动调用这个toString()
方法) - 所以如果需要清楚知道target的信息内容,可以单独抽离出来
- 在这里能够看到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个捕获器之前,了解捕获器是如何生效的更为重要
- 拦截机制:JS引擎会在内部为
Proxy
对象维护一个关联的目标对象和处理器对象。当对Proxy
对象进行操作时,这些操作首先被送到处理器对象 - 方法查找与执行:对于每种可以拦截的操作,如
get
、set
、apply
等,处理器对象可以提供一个同名的方法来拦截相应的操作,在处理器对象中查找到对应方法进行执行
- 拦截机制:JS引擎会在内部为
-
在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
可以拦截其目标对象上的所有get
和set
操作,而不仅仅是单个属性。非常适合于创建一个全面拦截和操作对象访问行为的模型,而我们通过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不是类,也不是构造函数或者函数对象,而是一个标准的内置对象,所以我们可以直接
- 那么这个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对象
- 这是因为在早期的ECMA规范中没有考虑到这种对
图24-4 Proxy/Handler与Reflect的方法对应
- 通过MDN文档所对应的关系,能够看到其关系紧密相连,并且在该文档中,也有详细对比了Object和Reflect
- Reflect 的方法通常返回更标准化的结果。在成功时,许多 Reflect 的操作会返回操作的结果(例如返回 true 或者属性值),在失败时返回 false,而不是抛出异常。这和 Object 的某些方法(如
Object.defineProperty
)在遇到错误时抛出异常的行为不同。这种设计在面对错误处理更加一致和可控 - Reflect内的方法作为和Math一样的静态方法,它的方法不会被任何对象继承。这种设计避免了在对象原型链中可能出现的混乱和冗余,确保了 Reflect 的方法仅用于反射和底层操作,而不会被意外地用于业务逻辑或其他目的
- Reflect和Proxy进行配合使用也非常的顺手,一一对应的关系,从使用角度上看,非常契合,因为在一开始设计的时候,这两者就注定相辅相成
- Reflect 的方法通常返回更标准化的结果。在成功时,许多 Reflect 的操作会返回操作的结果(例如返回 true 或者属性值),在失败时返回 false,而不是抛出异常。这和 Object 的某些方法(如
- 和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);
图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);//小余
图24-6 Proxy代理层拦截效果
- 在这里能够看到,Reflect借用Proxy拦截下
实际使用
到监听层
的时机,进行真正的处理- 查询获取顺序:实际使用->Proxy代理层get拦截->Reflect.get触发->监听层getter触发->从数据源中获取到数据->返回查询结果
- 改动顺序:实际使用->Proxy代理层set拦截->Reflect.set触发->监听层setter触发->修改数据源
- 在这里能够看到,
Proxy
的get
方法和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方法举例
- Proxy.get方法的Receiver参数是:Proxy自身代理
- Reflect.get方法是Receiver参数是:如果
target
对象中指定了getter
,receiver
则为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)
图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
图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
时能够访问到正确的属性,而不是错误地访问到代理对象或基对象的属性
- 这和this的原因很像,Proxy所返回的代理是固定的,例如我们的objProxy,虽然在大多数情况下可能是期望的行为,但这已经是限制死了,并不是动态决定,
表24-4 Proxy与Reflect中的receiver对比
特性 | Proxy get / set 中的 receiver | Reflect get / set 中的 receiver |
---|---|---|
作用 | 用于在 get /set 中处理属性时传递正确的 this 绑定,通常是代理对象 | 用于将 this 绑定传递到目标对象的 getter/setter,确保操作时正确绑定上下文 |
默认值 | 当调用 Proxy 的 get 或 set 时,receiver 通常是代理对象(即 Proxy 本身) | 需要显式传递,通常为调用时的对象,比如代理对象或者目标对象 |
用途 | 确保 getter/setter 中的 this 指向代理对象或自定义的对象,而非目标对象 | 允许将 this 绑定为代理对象或自定义对象,特别是在代理对象操作中,这能避免循环引用或错误行为 |
对于 getter 的影响 | receiver 确保 getter 中的 this 被正确绑定,尤其在继承链或代理链中传递正确的上下文 | 如果不传递 receiver ,Reflect.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中,将会如虎添翼,对数据的流动与改变,将会得心应手
- 框架会迭代,响应式会不断优化,但主要的枝干从不轻易改变,这是一个值得去深入掌握的思想原理!