当前位置: 首页 > news >正文

2024最新版JavaScript逆向爬虫教程-------基础篇之Proxy与Reflect详解

目录

  • 一、监听对象的操作
  • 二、Proxy基本使用
    • 2.1 创建空代理
    • 2.2 定义捕获器
      • 2.2.1 Proxy的set和get捕获器
      • 2.2.2 Proxy(handler)的13个捕获器
  • 三、Reflect的作用
    • 3.1 Reflect的使用
    • 3.2 Reflect其余方法(9个)
    • 3.3 Proxy与Reflect中的receiver参数
    • 3.4 Reflect中的construct方法

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。代理是目标对象的抽象。从很多方面看,代理类似 C++ 指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理施予的行为。注意: ECMAScript 代理与 C++ 指针有重大区别,后面会再讨论。不过作为一种有助于理解的类比,指针在概念上还是比较合适的结构。

Proxy 与 Reflect 在逆向补环境的时候有大量应用,建议重点掌握。

一、监听对象的操作

我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程,通过 2024最新版JavaScript逆向爬虫教程-------基础篇之面向对象 一文中的 2.2 对象属性操作的控制 小节了解到,我们可以通过属性描述符中的存储属性描述符 Object.defineProperty() 方法来做到,在 2024最新版JavaScript逆向爬虫教程-------基础篇之面向对象 一文 2.2 对象属性操作的控制 小节,有学习到访问器与数据两种不同的属性描述符,而通过其访问器分支 get、set,实现监听对象属性的操作。

const person = {name: "amo",age: 18
}let old_age = person.ageObject.defineProperty(person, "age", {get: function () {console.log('监听到该age属性被访问: ', old_age);},set: function (value) {console.log("监听到该age属性被设置", value);old_age = value;}
})person.age  //监听到该age属性被访问
console.log(person.age);//监听到该age属性被访问  undefined
person.age = 20//监听到该name属性被设置

运行结果如下图所示:
在这里插入图片描述
这里能够看到,不管是查看属性还是设置属性,都能够被 get 与 set 所捕获到。但这里有一个问题,那就是监听到属性了,为什么没返回正确的 age 属性的值,而是 undefined?这是因为查看属性会触发 get,而 get 没有返回内容,相当于 return 了一个 undefined,关于这一点,我们可以 return old_age 来试一下,如下图,且该描述在 MDN 文档中也有进行说明。
在这里插入图片描述
在这里插入图片描述
通过 get 监听案例,我们了解到如何监听到对象的操作(查看或者改变),也清楚的知道这是如何返回值的,在 get 监听案例中,我们返回了正确的 value 值,只需要拿到监听的 key,然后从对象中针对性获取即可,同时处理下当改变值时,在 set 中新值覆盖旧值就行,关键在于,该方式的局限性较大,只能监听一个属性,而一个对象中的属性在大多数情况下,都不止一个,此时有什么办法呢?我们目的从监听对象属性到监听对象的全部属性,首先我们需要先获取全部的 key 属性,然后遍历 key 属性填入 defineProperty 方法中,实现监听全部属性,示例代码:

const person = {name: "amo", age: 18, hobbies: ['drink']
}Object.keys(person).forEach(key => {let old_value = person[key]Object.defineProperty(person, key, {get: function () {console.log(`监听到${key}属性被访问`);return old_value}, set: function (value) {console.log(`监听到${key}属性被设置值`);old_value = value}})
})person.name//监听到name属性
person.age//监听到age属性
person.hobbies//监听到hobbies属性person.name = 'paul'//监听到name属性被设置值
person.age = 28//监听到age属性被设置值
person.hobbies = ['read']//监听到hobbies属性被设置值console.log(person.name);//paul
console.log(person)
console.log(person.hobbies);

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

二、Proxy基本使用

2.1 创建空代理

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。代理是使用 Proxy 构造函数创建的,这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出 TypeError。要创建空代理,可以传一个简单的对象字面量作为处理程序对象, 从而让所有操作畅通无阻地抵达目标对象。如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象。

const target = {id: 'target'
};const handler = {};const proxy = new Proxy(target, handler);// id属性会访问同一个值
console.log(target.id);  // target
console.log(proxy.id);   // target//给目标属性赋值会反映在两个对象上
//因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id);  // foo//给代理属性赋值会反映在两个对象上
//因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id);  // bar// hasOwnProperty()方法在两个地方
//都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id'));  // true// Proxy.prototype是undefined
//因此不能使用instanceof操作符
// TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(target instanceof Proxy); 
// TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy);  //严格相等可以用来区分代理和目标
console.log(target === proxy); // false

2.2 定义捕获器

2.1 创建空代理 我们了解到,在 ES6 中,新增了一个 Proxy 类(构造函数), 这个类从名字就可以看出来,是用于帮助我们创建一个代理的,代理在维基百科上解释为代表授权方处理事务,在这里可以理解为 Proxy 代表对象处理监听的相关操作,也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy 对象),之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作。我们可以将 一、监听对象的操作 小节中的 defineProperty 案例用 Proxy 来实现一次(监听 person 对象):

const p = new Proxy(target, handler)//监听的对象 处理对象

其 target 参数为授权方,也就是要使用 Proxy 包装的目标对象(侦听对象),该目标对象可以是任何类型的对象,包括原生数组,函数,甚至另一个代理。首先,我们需要 new Proxy() 构造函数,并且传入需要侦听的对象以及一个处理对象,可以称之为 handler,在我们这个案例中,objProxy 一开始是没有和 obj 对象产生联系的,通过 target 参数,去代理 obj 对象,此时对 obj 的所有操作,都应该通过 objProxy 操作了,我们对 objProxy 的所有操作,最终效果都会作用在 obj 对象身上(这个在 2.1 创建空代理 小节已经演示过,这里不再赘述),那多了一层代理,像 defineProperty 方法完成正常 "查询"、"改动" 操作时,我们也能够在这一些关键时机去处理一些事情。

const person = {name: "amo",age: 18
}//1.创建一个Proxy对象,Proxy对象是一个构造函数,所以使用new创建
let handler = {}  // 暂时还是先定义一个空的处理程序对象
const objProxy = new Proxy(person, {})
//2.对person的所有操作,都应该去操作objProxy
console.log(objProxy.name); // amo
objProxy.age = 20
console.log(objProxy.age) // 20
console.log(person) // 已经被修改了 { name: 'amo', age: 20 }
// console.log(person.name);

我们之后的操作都是直接对 Proxy 的操作,而不是原有的对象,而对 Proxy 的操作,需要使用到第二个参数 handler,通常 handler 被称为处理对象,可以理解为处理 Proxy 操作的对象,也有 handler 被译为处理器的,在 handler 中,有众多的捕获器,用来捕捉 "查找"、"改动" 这个变化过程的一些时机,不同的捕获器针对于不同的时机和具备不同作用,默认情况下,在我们还不了解有什么捕获器时,我们可以给一个空对象,依旧可以通过操作 objProxy 代理从而作用于 person 对象身上,这并不影响代理的作用,只是没有使用捕获器介入这一过程做出其他事情。

2.2.1 Proxy的set和get捕获器

我们如果想要介入该操作过程,最主要的是以下两个捕获器:

  1. handler.get:属性读取操作的捕捉器
  2. handler.set:属性设置操作的捕捉器

这两捕获器作用,和一开始我们使用 defineProperty 的 get、set 是相同的,在这个基础上,我们可以继续完善 Proxy 的这个案例代码,这两个捕获器也会接受到对应的参数,get:trapTarget(监听对象)、property(监听对象的属性),receiver(代理对象),set:trapTarget(监听对象)、property(监听对象的属性)、value(改动的新值),receiver(代理对象),通过两个捕获器的这些参数实现 "查找""改变" 操作,在这里能够看到 trapTarget 在模板字符串中,被解析为 [object Object],会被以 toString() 方法的形式进行解析(当对象需要被转换成字符串形式,如在模板字符串或字符串拼接中使用时,JS 自动调用这个 toString() 方法),所以如果需要清楚知道 trapTarget 的信息内容,可以单独抽离出来。

const person = {name: "amo",age: 10,hobbies: 'drink'
}const objProxy = new Proxy(person, {get: function (trapTarget, property, receiver) {console.log(`监听到${trapTarget}对象的${property}属性被访问了`);console.log(trapTarget, '单独抽离出来');//{ name: 'jerry', age: 10, hobbies: 'drink' }  单独抽离出来return trapTarget[property]},set: function (trapTarget, property, value, receiver) {console.log(`监听到${trapTarget}对象的${property}属性被设置值了`);trapTarget[property] = value}
})objProxy.name = 'jerry'//监听到[object Object]对象的name属性被设置值了
console.log(objProxy.name);//监听到[object Object]对象的name属性被访问了 jerry

此时我们再来回顾 handler 参数:一个对象,其属性是定义代理在对其执行操作时的行为的函数,首先这是一个对象,默认情况下是空对象,在该情况下,对 Proxy 代理的任何操作都和直接对监听对象的操作没有任何区别,在该对象里面有各种属性,这些属性会在我们对 Proxy 代理执行操作时,拦截下对应的一些操作,handler 对象内的属性又称为拦截器,一共13个,都可以算成 handler 对象的实例方法,每一个 handler 实例方法都是和正常对象的某个实例方法对应上,从而实现拦截。例如我们实现监听对象的 "查找""改动",这两个操作,不管是 defineProperty 还是 Proxy 的实现方式,都是使用 set 与 get 方法,我们可以简单理解为狸猫换太子。最后,我们还要说明 set 与 get 分别对应的是函数类型,最后的参数 receiver,该参数指:Proxy 或者继承 Proxy 的对象,具体做什么的,我们暂时跳过,等下再回头来看。

2.2.2 Proxy(handler)的13个捕获器

一个空的处理器(handler)将会创建一个与被代理对象行为几乎完全相同的代理对象。通过在 handler 对象上定义一组函数,我们可以自定义被代理对象的一些特定行为,在这里需要注意,几乎完全相同的代理对象,这意味使用方式和行为是一致的,每个 捕获器(trap) 对应于一个特定的对象操作。如果处理器对象有相应的捕获方法,则该方法会被调用;如果没有,操作会直接转发给目标对象,捕获器中的逻辑决定了是否将操作重定向至目标对象,是否修改操作的行为,或是否直接返回一个自定义的结果。在正式讲解这13个捕获器之前,了解更为重要

捕获器的拦截机制:JS引擎会在内部为Proxy对象维护一个关联的目标对象和处理器对象。当对Proxy对象进行操作时,这些操作首先被送到处理器对象,方法查找与执行:对于每种可以拦截的操作,如get、set、apply等,处理器对象可以提供一个同名的方法来拦截相应的操作,在处理器对象中查找到对应方法进行执行。

在13个捕获器中,有4个常用的,其中两个是已经讲过的set、get,另外两个是 has 与 deleteProperty,这四个捕获器涵盖了对对象进行读取、写入和属性检查的基本操作,这些是日常编程中最常见的操作。几乎所有涉及对象属性的交互都会触及到这些操作,包括访问、修改、检查属性是否存在,以及删除属性,如下图所示:

示例代码:

const obj = {name: "amo",age: 20
}const objProxy = new Proxy(obj, {//代理objset: function (target, property, value) {console.log(`监听:监听${property}的设置值`);target[property] = value},get: function (target, property) {console.log(`监听:监听${property}的获取`);return target[property]},deleteProperty: function (target, property) {console.log(`监听:监听删除${property}属性`);delete obj.name},has: function (target, property) {console.log(`监听:监听in判断${property}属性`);return property in target}
})delete objProxy.name
console.log("age" in objProxy);

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

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 对象。
在这里插入图片描述
通过 MDN 文档所对应的关系,能够看到其关系紧密相连,并且在该文档中,也详细对比了 Object 和 Reflect:

  1. Reflect 的方法通常返回更标准化的结果。在成功时,许多 Reflect 的操作会返回操作的结果(例如返回 true 或者属性值),在失败时返回 false,而不是抛出异常。这和 Object 的某些方法(如 Object.defineProperty)在遇到错误时抛出异常的行为不同。这种设计在面对错误处理更加一致和可控
  2. Reflect 内的方法作为和 Math 一样的静态方法,它的方法不会被任何对象继承。这种设计避免了在对象原型链中可能出现的混乱和冗余,确保了 Reflect 的方法仅用于反射和底层操作,而不会被意外地用于业务逻辑或其他目的
  3. Reflect 和 Proxy 进行配合使用也非常的顺手,一一对应的关系,从使用角度上看,非常契合,因为在一开始设计的时候,这两者就注定相辅相成

和 Proxy 进行对应,简单练习一下最常见的 set、get、has、deleteProperty 这四个方法,示例代码:

// 定义一个简单的对象
const obj = {name: "amo",age: 18
};// 使用 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'));  // Name: amo
console.log("Age:", Reflect.get(obj, 'age'));  // Age: 18// 如果属性不存在,可以提供一个默认值 测试失败
// console.log("Gender:", Reflect.get(obj, 'gender', {'gender': 'Not specified'}));  // 输出 "Not specified"
// 实际输出undefined// 使用 Reflect.set() 设置对象属性的值
Reflect.set(obj, 'age', 19);  // 设置 age 属性为 19
console.log("Updated Age:", obj.age);  // 输出 19// 使用 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

3.1 Reflect的使用

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

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

3.2 Reflect其余方法(9个)

Reflect 剩余的9个方法,此处只会简单进行说明,有用到再来翻阅即可:

//1.用于获取一个对象的原型(也称为 prototype)该方法返回该对象的原型对象,即该对象继承的对象。
//如果该对象没有继承任何对象,则返回 null。
Reflect.getPrototypeOf(target) ⇒ Object.getPrototypeOf()
//2.设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true
Reflect.setPrototypeOf(target, prototype)
//3.用于判断一个对象是否可以被扩展,即是否可以添加新的属性
//该方法返回一个布尔值,表示该对象是否可以被扩展,即是否可以通过,Object.defineProperty()或者直接赋值添加新的属性
Reflect.isExtensible(target) ⇒ Object.isExtensible()
//4.返回一个Boolean,用于阻止一个对象被扩展,即不允许添加新的属性
//该方法返回一个布尔值,表示该对象是否被阻止了扩展,即是否不允许添加新的属性
//其中,target 是要阻止扩展的对象。
Reflect.preventExtensions(target) ⇒ Object.preventExtensions()
//5.如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined
Reflect.getOwnPropertyDescriptor(target, propertyKey) ⇒ Object.getOwnPropertyDescriptor()
//6.如果设置成功就会返回 true
Reflect.defineProperty(target, propertyKey, attributes) ⇒ Object.defineProperty()
//7.返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响)
Reflect.ownKeys(target)
//8.对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和
//Function.prototype.apply() 功能类似
Reflect.apply(target, thisArgument, argumentsList)
//9.对构造函数进行 new 操作,相当于执行 new target(...args)
Reflect.construct(target, argumentsList[, newTarget])

3.3 Proxy与Reflect中的receiver参数

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

const obj = {_name: 'amo',get name() {return this._name},set name(newValue) {this._name = newValue}
}obj.name = 'jerry'
console.log(obj.name); // jerry

对象的 setter、getter 的监听层效果:
在这里插入图片描述
此时加上我们的 Proxy 代理层和对应的 get、set 捕获器,以及对应的 Reflect 的 set、get 方法来实现,相对于一开始的最初数据源、监听层、实际使用,多了一层代理层(Proxy),示例:

const obj = {_name: 'amo',get name() {return this._name},set name(newValue) {this._name = newValue}
}//代理层
const objProxy = new Proxy(obj, {get: function (target, property) {return Reflect.get(target, property)},set: function (target, property, value) {Reflect.set(target, property, value)}
})//实际使用
objProxy.name = 'jerry'
console.log(objProxy.name); // jerry
console.log(obj.name); // jerry

Proxy 代理层拦截效果:
在这里插入图片描述
在这里能够看到,Reflect 借用 Proxy 拦截下实际使用到监听层的时机,进行真正的处理,查询获取顺序: 实际使用 ⇒ Proxy 代理层 get 拦截 ⇒ Reflect.get 触发 ⇒ 监听层 getter 触发 ⇒ 从数据源中获取到数据 ⇒ 返回查询结果。改动顺序: 实际使用 ⇒ Proxy 代理层 set 拦截 ⇒ Rflect.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: 'amo',get name() {return this._name},set name(newValue) {this._name = newValue}
}const objProxy = new Proxy(obj, {get: function (target, property) {console.log('被访问:', target, property);return Reflect.get(target, property)},set: function (target, property, newValue) {console.log('被设置:', target, property);Reflect.set(target, property, newValue)}
})
//这在Reflect.set触发之前打印的,所以输出的_name为未修改状态,在浏览器控制台则为最终结果amo
objProxy.name = 'jerry'//被设置: { _name: 'amo', name: [Getter/Setter] } name
console.log(objProxy.name);//被访问: { _name: 'jerry', name: [Getter/Setter] } name
// jerry

如果我们使用了 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: 'amo',get name() {return this._name},set name(newValue) {this._name = newValue}
}const objProxy = new Proxy(obj, {get: function (target, property, receiver) {console.log('被访问:', target, property);return Reflect.get(target, property, receiver)},set: function (target, property, newValue, receiver) {console.log('被设置:', target, property);Reflect.set(target, property, newValue, receiver)}
})
//这在Reflect.set触发之前打印的,所以输出的_name为未修改状态,在浏览器控制台则为最终结果amo
objProxy.name = 'jerry'
// 被设置: { _name: 'amo', name: [Getter/Setter] } name
// 被设置: { _name: 'amo', name: [Getter/Setter] } _name
console.log(objProxy.name); // jerry
// 被访问: { _name: 'jerry', name: [Getter/Setter] } name
// 被访问: { _name: 'jerry', name: [Getter/Setter] } _name

可以看到在加上 receiver 之后,objProxy 代理的 get、set 方法都被调用了两次,在这两次结果中,通过 key,我们能察觉到有所不同,一个是 name,一个是 _name,在第一次拦截中,是正常在 Proxy 调用了 Reflect 的 set 与 get,这是与监听层形成交互,此时的 property 是指 obj 中的 name 方法(setter、getter),Proxy 代理层调用 Reflect 与监听层形参交互:

主要在第二次拦截中,监听层的 this 被 Reflect 的 receiver 所改变,变为 Proxy 代理本身,此时在 obj 中的代码就会变为如下形式:

const obj = {_name: 'amo',get name() {return objProxy._name//{ _name: 'amo', name: [Getter/Setter] }._name},set name(newValue) {objProxy._name = newValue}
}

而 objProxy 的内容是: { _name: 'amo', name: [Getter/Setter] },此时访问的则是里面的 _name,这时候我们再来回头看打印内容,就会发现一目了然。两次输出,意味着 objProxy 在两个不同的地方被调用了,一次在 Proxy 代理层,一次在监听层,Proxy 和 Reflect 的 receiver 做到了替换掉 obj 对象中的 this,从而进一步提高拦截的完整度:

objProxy.name = 'jerry'
// 被设置: { _name: 'amo', name: [Getter/Setter] } name
// 被设置: { _name: 'amo', name: [Getter/Setter] } _name
console.log(objProxy.name); // jerry
// 被访问: { _name: 'jerry', name: [Getter/Setter] } name
// 被访问: { _name: 'jerry', name: [Getter/Setter] } _name

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 时能够访问到正确的属性,而不是错误地访问到代理对象或基对象的属性,Proxy 与 Reflect 中的 receiver 对比:

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 = namethis.age = age
}function Teacher(name, age) {}const stu = new Student('amo', 20)
console.log(stu);//Student { name: 'amo', 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, ['amo', 18], Teacher)
console.log(teacher);//Teacher { name: 'amo', age: 18 }
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)}
}

相关文章:

2024最新版JavaScript逆向爬虫教程-------基础篇之Proxy与Reflect详解

目录 一、监听对象的操作二、Proxy基本使用2.1 创建空代理2.2 定义捕获器2.2.1 Proxy的set和get捕获器2.2.2 Proxy(handler)的13个捕获器 三、Reflect的作用3.1 Reflect的使用3.2 Reflect其余方法(9个)3.3 Proxy与Reflect中的receiver参数3.4 Reflect中的construct方法 ECMAScr…...

代码修改材质参数

1、 如何得到对象使用的材质 获取到对象的渲染器Renderer Mesh Renderer和Skinned Mesh Renderer都继承Renderer,可以用里式替换原则父类获取、装载子类对象 通过渲染器获取到对应材质 可以利用渲染器中的material或者sharedMaterial来获取物体的材质&#xff0…...

[C++11] 包装器 : function 与 bind 的原理及使用

文章目录 functionstd::function 的基本语法使用 std::function 包装不同的可调用对象function包装普通成员函数为什么要传入 this 指针参数?传入对象指针与传入对象实例的区别 例题 :150. 逆波兰表达式求值 - ⼒扣(LeetCode) bin…...

java项目-jenkins任务的创建和执行

参考内容: jenkins的安装部署以及全局配置 1.编译任务的general 2.源码管理 3.构建里编译打包然后copy复制jar包到运行服务器的路径 clean install -DskipTests -Pdev 中的-Pdev这个参数用于激活 Maven 项目中的特定构建配置(Profile) 在 pom.xml 文件…...

单片机中的BootLoader(重要的概念讲解)

文章目录 一、链接地址和执行地址1. 链接地址(Load Address)2. 执行地址(Execution Address)链接地址与执行地址的关系实际工作流程总结二、相对跳转和绝对跳转1. 相对跳转(Relative Jump)2. 绝对跳转(Absolute Jump)3. `BX` 和 `BL` 指令总结三、散列文件1. 散列文件的…...

【数据分享】中国食品工业年鉴(1984-2023) PDF

数据介绍 一、《中国食品工业年鉴》(以下简称《年鉴》)是一部全面反映上一年度全国食品工业发展情况纪年性、资料性、权威大型年刊。《年鉴(2023)》系统收录了全国食品行业各专业和 31个省(自治区、直辖市)2022年食品工业经济运行情况的综述,《年鉴》是由中国食品工…...

优选算法 - 1 ( 双指针 移动窗口 8000 字详解 )

一&#xff1a;双指针 1.1 移动零 题目链接&#xff1a;283.移动零 class Solution {public void moveZeroes(int[] nums) {for(int cur 0, dest -1 ; cur < nums.length ; cur){if(nums[cur] 0){}else{dest; // dest 先向后移动⼀位int tmp nums[cur];nums[cur] num…...

FairyGUI和Unity联动(入门篇)

一、FairyGUI编辑器中 1.新建按钮、新建组件 编辑器中界面简易设计如下 2.文件-发布设置-发布路径&#xff1a;自己unity项目Resources所在的路径 二、Unity 使用代码展示UI using FairyGUI; using System.Collections; using System.Collections.Generic; using UnityEngi…...

Go:文件输入输出以及json解析

文章目录 读取用户的输入文件读写读文件写文件 文件拷贝io包中接口的概念JSON 数据格式编码解码任意的数据&#xff1a; 读取用户的输入 从键盘和标准输入 os.Stdin 读取输入&#xff0c;最简单的办法是使用 fmt 包提供的 Scan… 和 Sscan… 开头的函数 看如下的程序 func t…...

编写红绿起爆线指标(附带源码下载)

编写需求&#xff1a; 想问问有没有能标注行情起爆点的指标。 效果展示&#xff1a; 红线上&#xff0c;出现绿柱转红柱做多。 蓝线下&#xff0c;出现红柱转绿柱做空。 源码展示&#xff08;部分源码&#xff0c;完整源码需下载源码文件&#xff09;&#xff1a; IsMainIn…...

设计模式(四)装饰器模式与命令模式

一、装饰器模式 1、意图 动态增加功能&#xff0c;相比于继承更加灵活 2、类图 Component(VisualComponent)&#xff1a;定义一个对象接口&#xff0c;可以给这些对象动态地添加职责。ConcreteComponent(TextView)&#xff1a;定义一个对象&#xff0c;可以给这个对象添加一…...

Android11 修改系统语言

1.定义一个view <RelativeLayoutandroid:id"id/rlChooseLanguage"style"style/SettingAboutItem"><TextViewstyle"style/SettingAboutItemTextView"android:text"string/choose_language" /><ImageView style"st…...

vue3 查看word pdf excel文件

也是在网上找的基础上修改的 可以直接使用 npm install vue-office/docx npm install vue-office/excel npm install vue-office/pdf​<template><divclass"Office-Preview"v-loading"loading"element-loading-text"文件加载中...">…...

java八股-垃圾回收机制-垃圾回收算法,分代回收,垃圾回收器

文章目录 垃圾回收算法引用计数法可达性分析算法 jvm垃圾回收算法标记清除算法标记整理算法复制算法本章总结 JVM中的分代回收本章总结 JVM有哪些垃圾回收器&#xff1f;1.串行垃圾收集器2.并行垃圾收集器3.CMS&#xff08;并发&#xff09;垃圾收集器本章小结 详细聊一下G1垃圾…...

iSCSI 和FC的概述

一、技术基础与架构 iSCSI 技术基础&#xff1a;iSCSI是基于TCP/IP协议的存储网络协议&#xff0c;它实现了在IP网络上运行SCSI协议。架构&#xff1a;iSCSI协议栈包括SCSI层、iSCSI层、TCP/IP层等&#xff0c;通过标准的以太网技术实现存储数据的传输。 FC 技术基础&#xff1…...

一文了解Android中的AudioFlinger

AudioFlinger 是 Android 音频框架中的一个核心组件&#xff0c;负责管理音频流的混合和输出。它是 Android 音频系统服务的一部分&#xff0c;作为音频框架和硬件之间的桥梁&#xff0c;接收应用程序的音频请求、进行混音处理&#xff0c;并最终通过音频硬件输出声音。 ![在这…...

超全面!一文带你快速入门HTML,CSS和JavaScript!

作为一名后端程序员&#xff0c;在开发过程中避免不了和前端打交道&#xff0c;所以就要了解一些前端的基础知识&#xff0c;比如三剑客HTML,CSS,JavaScript&#xff0c;甚至有必要学习一下Vue、React等前端主流框架。 学习文档&#xff1a;https://www.w3school.com.cn/ 一…...

C语言 | Leetcode C语言题解之第557题反转字符串中的单词III

题目&#xff1a; 题解&#xff1a; char* reverseWords(char* s) {int length strlen(s);char* ret (char*)malloc(sizeof(char) * (length 1));ret[length] 0;int i 0;while (i < length) {int start i;while (i < length && s[i] ! ) {i;}for (int p …...

408笔记合集

操作系统 《王道操作系统》-BitHachi 计算机网络 《王道计算机网络》--BitHachi 组成原理 《王道计算机组成原理》--BitHachi...

智慧医疗:纹理特征VS卷积特征

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…...

OPC学习笔记

一. 解决使用milo读取OPC设备字符串类型时&#xff0c;出现中文和特殊符号乱码的情况 解决前&#xff0c;读取字符串&#xff1a;你好 2. 解决后&#xff0c;读取字符串&#xff1a;你好 3. 解决前&#xff0c;读取字符串&#xff1a;165℃ 解决后&#xff0c;读取字符串&am…...

数据结构的时间复杂度和空间复杂度

目录 时间复杂度 空间复杂度 时间复杂度 基本操作的执行次数&#xff0c;为时间复杂度。 我们使用大O的渐进表示法来表示时间复杂度。 怎么使用&#xff1f; 先看例子&#xff1a; 在这个例子中&#xff0c; 基本操作为变量 count 的 加加 操作&#xff0c;并且&#xff0c;执行…...

HBase理论_背景特点及数据单元及与Hive对比

本文结合了个人的笔记以及工作中实践经验以及参考HBase官网&#xff0c;我尽可能把自己的知识点呈现出来&#xff0c;如果有误&#xff0c;还请指正。 1. HBase背景 HBase作为面向列的数据库运行在HDFS之上&#xff0c;HDFS缺乏随机读写操作&#xff0c;HBase正是为此而出现。…...

生产模式打包

在生产模式下打包 Node.js 和前端&#xff08;例如 Vue 或 React&#xff09;应用时&#xff0c;通常需要对代码进行优化&#xff0c;使其在生产环境中运行更高效。以下是如何在生产模式下配置和打包项目的步骤&#xff1a; 1. Node.js 生产模式打包 Node.js 本身不需要像前端…...

Vue的路由

Vue的路由 出发点&#xff1a;遇到多页面网页的反复跳转&#xff0c;有些繁琐&#xff0c;可以通过Vue的路由实现单页面中数据的变化 实现单页面中数据的变化&#xff08;通过Vue-router来进行操作的&#xff0c;数据的请求获取也需要ajax异步交互&#xff09;&#xff0c;具…...

Spring框架之策略模式 (Strategy Pattern)

策略模式&#xff08;Strategy Pattern&#xff09;详解 策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;用于定义一系列算法&#xff0c;并将每种算法封装到独立的策略类中&#xff0c;使它们可以相互替换&#xff0c;从而使算法的变化独…...

探索Google Earth Engine:利用MODIS数据和R语言进行2000-2021年遥感生态指数(RSEI)的时空趋势分析

前段时间,小编学习了在GEE上进行遥感生态指数(RSEI)的评估,非常头疼,但是实验了两周后,亲测有效,主要采用的是MODIS数据分析了2000-2021年中国内蒙古某地的RSEI时间序列分布状况,现在把学习的代码分享给大家。 1 GEE计算RSEI 1.1研究区域导入与初步定义 var sa = ee…...

多商户中英双语电商系统设计与开发 PHP+mysql

随着全球电商市场的扩展&#xff0c;多商户平台成为了越来越多商家参与全球贸易的重要方式。为了适应不同语言用户的需求&#xff0c;尤其是中英双语用户的需求&#xff0c;设计一个支持中英双语的电商系统显得尤为重要。本文将重点探讨如何设计一个多商户中英双语电商系统&…...

牵手App红娘专属1V1服务,打造贴心交友指导

对于年轻一代而言&#xff0c;婚恋方式已明显区别于传统&#xff0c;他们更倾向于直接、活泼的交流方式&#xff0c;享受着在轻松愉快的氛围中边玩边交友的乐趣。线上社交平台&#xff0c;尤其是那些基于兴趣构建的交友模式&#xff0c;正逐渐成为他们探索爱情、寻找共鸣的新舞…...

论文解析:边缘计算网络中资源共享的分布式协议(2区)

目录 论文解析:边缘计算网络中资源共享的分布式协议(2区) 核心内容: 核心创新点的原理与理论: 多跳边缘计算场景 一、边缘计算的基本概念 二、多跳边缘计算场景的含义 三、多跳边缘计算场景的应用 四、多跳边缘计算场景的优势 论文解析:协作边缘计算网络中资源共…...