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

目录

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

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

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

一、监听对象的操作

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

javascript 复制代码
const person = {
    name: "amo",
    age: 18
}

let old_age = person.age

Object.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 方法中,实现监听全部属性,示例代码:

javascript 复制代码
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。要创建空代理,可以传一个简单的对象字面量作为处理程序对象, 从而让所有操作畅通无阻地抵达目标对象。如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象。

javascript 复制代码
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 对象):

javascript 复制代码
const p = new Proxy(target, handler)//监听的对象 处理对象

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

javascript 复制代码
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 的信息内容,可以单独抽离出来。

javascript 复制代码
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,这四个捕获器涵盖了对对象进行读取、写入和属性检查的基本操作,这些是日常编程中最常见的操作。几乎所有涉及对象属性的交互都会触及到这些操作,包括访问、修改、检查属性是否存在,以及删除属性,如下图所示:

示例代码:

javascript 复制代码
const obj = {
    name: "amo",
    age: 20
}

const objProxy = new Proxy(obj, {//代理obj
    set: 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 中操作对象的方法,比如:

javascript 复制代码
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 这四个方法,示例代码:

javascript 复制代码
// 定义一个简单的对象
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 进行取代使用,示例代码:

javascript 复制代码
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个方法,此处只会简单进行说明,有用到再来翻阅即可:

javascript 复制代码
//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 正确的使用逻辑,示例代码:

javascript 复制代码
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),示例:

javascript 复制代码
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 指向问题:

javascript 复制代码
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 方法举例:

javascript 复制代码
// Proxy.get方法的Receiver参数是: Proxy自身代理
// Reflect.get方法是Receiver参数是: 如果target对象中指定了getter,receiver则为getter调用时的this值

这就可以结合起来了,Proxy.get 的 Receiver 参数提供确定的 this 值,Reflect.get 的 Receiver 参数提供 this 放置的位置,在这方面上,get 与 set 是一样的,此时,我们再来进行刚才的 set 设置,等实践完成,我们再来进行说明:

javascript 复制代码
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 中的代码就会变为如下形式:

javascript 复制代码
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,从而进一步提高拦截的完整度:

javascript 复制代码
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):

javascript 复制代码
//target:被运行的目标构造函数
//argumentsList:类数组,目标构造函数调用时的参数
//newTarget:作为新创建对象的原型对象的 constructor 属性
Reflect.construct(target, argumentsList[, newTarget])//有返回值

想要知道 construct 方法的作用,我们需要举一个应用场景来说明,在下方案例中,Student 是一个构造函数,通过 Student 所 new 出来的对象,自然是 Student 类型,现在有一个需求,我希望 new Student,结果是 Teacher 类型,在以前是很难做到的,需要进行额外的操作,例如使用工厂函数:

javascript 复制代码
function Student(name, age) {
    this.name = name
    this.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 的原型,这是非常灵活的继承类型,打破了传统构造函数和原型继承的限制。但同时我们也应该清楚,越是灵活,就越是双刃剑,在一般情况下,我们是用不到该方法的:

javascript 复制代码
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 调用一样的效果:

javascript 复制代码
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)
    }
}
相关推荐
聊无生21 分钟前
JavaSrcipt 函数高级
开发语言·前端·javascript
小安同学iter30 分钟前
Java进阶五 -IO流
java·开发语言·intellij-idea
Hello World and You30 分钟前
R ggplot2 绘图细节 geom_text展示不全 y轴坐标细节 x轴标题
开发语言·r语言
sukalot34 分钟前
windows C#-异步文件访问
开发语言·c#
熬夜学编程的小王1 小时前
【C++篇】从基础到进阶:全面掌握C++ List容器的使用
开发语言·c++·list·双向链表·迭代器失效
悄悄敲敲敲1 小时前
C++:智能指针
开发语言·c++
B站计算机毕业设计超人1 小时前
计算机毕业设计SparkStreaming+Kafka新能源汽车推荐系统 汽车数据分析可视化大屏 新能源汽车推荐系统 汽车爬虫 汽车大数据 机器学习
数据仓库·爬虫·python·数据分析·kafka·数据可视化·推荐算法
书埋不住我1 小时前
java第三章
java·开发语言·servlet
好开心331 小时前
javaScript交互案例2
开发语言·前端·javascript·html·ecmascript·交互
tian-ming1 小时前
(十八)JavaWeb后端开发案例——会话/yml/过滤器/拦截器
java·开发语言·前端