JS 中Proxy代理和 Reflect反射方法示例详解

JavaScript/前端
362
0
0
2023-03-19
目录
  • 正文
  • 1.属性描述符
  • 2.Reflect
  • 3.Proxy
  • 3.1 创建空代理
  • 3.2 定义捕获器
  • 3.3 捕获器不变式
  • 3.4 可撤销代理
  • 4.代理捕获器与反射方法
  • 4.1 get()
  • 4.2 set()
  • 4.3 has()
  • 4.4 deleteProperty()
  • 4.5 apply()
  • 4.6 construct()

正文

总所周知,Vue2 => Vue3 时,数据响应式方法从Object.defineProperty()方法变成了Proxy(),所以今天与大家 Proxy(代理)和 Reflect(反射)的知识。

讲解 Proxy 和 Reflect 前,我们需要先了解属性描述符的作用,所以我们线简单解释一下属性描述符的知识。

1.属性描述符

属性描述符(Property Descriptor) 本质上是一个 JavaScript 普通对象,用于描述一个属性的相关信息,共有下面这几种属性。

  • value:属性值
  • configurable:该属性的描述符是否可以修改
  • enumerable:该属性是否可以被枚举
  • writable:该属性是否可以被重新赋值
  • 存取器属性:属性描述符中如果配置了 get 和 set 中的任何一个,则该属性不再是一个普通属性,而变成了存取器属性
  • get()读值函数:如果一个属性是存取器属性,则读取该属性时,会运行 get 方法,并将 get 方法得到的返回值作为属性值
  • set(newVal)存值函数:如果给该属性赋值,则会运行 set 方法,newVal 参数为赋值的值。

存取器属性最大的意义,在于可以控制属性的读取和赋值,在函数里可以进行各种操作。

Vue2 数据响应式就是使用了这一点,在 getter 和 setter 函数中进行了数据绑定与派发更新。

注意点:value 和 writable 属性不能与 get 和 set 属性二者不可共存,二者只能选其一。

查看某个对象的属性描述符,使用以下这两种方法:

Object.getOwnPropertyDescriptor(对象, 属性名); //得到一个对象的某个属性的属性描述符
Object.getOwnPropertyDescriptors(对象); //得到某个对象的所有属性描述符

为某个对象添加属性时 或 修改属性时,配置其属性描述符,使用以下这两种方法:

Object.defineProperty(对象, 属性名, 描述符); //设置一个对象的某个属性
Object.defineProperties(对象, 多个属性的描述符); //设置一个对象的多个属性

2.Reflect

Reflect 是什么? Reflect 是一个内置的 JS 对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能。

由于它类似于其他语言的反射,因此取名为 Reflect。

它可以做什么? 使用 Reflect 可以实现诸如:属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中 等等功能。

这些功能不是已经存在了吗?为什么还需要用 Reflect 实现一次? 有一个重要的理念,在 ES5 就被提出:减少魔法、让代码更加纯粹(语言的方法使用 API 实现,而不使用特殊语法实现),这种理念很大程度上是受到函数式编程的影响。 ES6 进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的 API,并高度聚合到某个对象中,于是就造就了 Reflect 对象。 因此,你可以看到 Reflect 对象中有很多的 API 都可以使用过去的某种语法或其他 API 实现。

Reflect 里面提供了哪些 API 呢?

Reflect API

用处

等同于

Reflect.get(target, propertyKey)

读取对象 target 的属性 propertyKey

对象的属性值读取操作

Reflect.set(target, propertyKey, value)

设置对象 target 的属性 propertyKey 的值为 value

对象的属性赋值操作

Reflect.has(target, propertyKey)

判断一个对象是否拥有一个属性

in 操作符

Reflect.defineProperty(target, propertyKey, attributes)

类似于 Object.defineProperty,不同的是如果配置出现问题,返回 false 而不是报错

Object.defineProperty

Reflect.deleteProperty(target, propertyKey)

删除一个对象的属性

delete 操作符

Reflect.apply(target, thisArgument, argumentsList)

调用一个指定的函数,并绑定 this 和参数列表

函数调用操作

Reflect.construct(target, argumentsList)

用构造函数的方式创建一个对象

new 操作符

其他更多的 Reflect API

3.Proxy

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力

具体地说,可以给目标对象(target)定义一个关联的代理对象,而这个代理对象可当作一个抽象的目标对象来使用

因此在对目标对象的各种操作影响到目标对象之前,我们可以在代理对象中对这些操作加以控制,并且最终也可能会改变操作返回的结果。

所以我的理解是:代理(Proxy)能使我们开发者拥有一种间接修改底层方法的能力,从而控制用户的操作。

3.1 创建空代理

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。 默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。

代理是使用 Proxy 构造函数创建的,这个构造函数接收两个参数:目标对象和处理程序对象。缺 少其中任何一个参数都会抛出 TypeError。返回一个代理对象 如:new Proxy(target, handler);

要创建空代理,可以传一个简单的对象字面量作为处理程序对象,从而让所有操作畅通无阻地抵达目标对象。

const target = {
 id: 'target'
}; //target:目标对象
const handler = {}; //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 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype
'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype
'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false

3.2 定义捕获器

使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。

每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。

每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get()捕获器会接收到目标对象要查询的属性代理对象三个参数。

const target = {
  foo: "bar",
};
const handler = {
  // 捕获器在处理程序对象中以方法名为键
  get(trapTarget, property, receiver) {
    //trapTarget - 目标对象
    //property   - 要查询的属性
    //receiver   - 代理对象
    return "handler override";
  },
};
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override

3.3 捕获器不变式

使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。

根据 ECMAScript 规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:

const target = {};
Object.defineProperty(target, "foo", {
  configurable: false,
  writable: false,
  value: "bar",
});
const handler = {
  get() {
    return "qux";
  },
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // TypeError

3.4 可撤销代理

有时候可能需要中断代理对象与目标对象之间的联系。

Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。

后续可直接调用撤销函数 revoke() 来撤销代理。

撤销代理之后再调用代理会抛出 TypeError,撤销函数和代理对象是在实例化时同时生成的:

const target = {
  foo: "bar",
};
const handler = {
  get() {
    return "intercepted";
  },
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError

4.代理捕获器与反射方法

4.1 get()

get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()

const myTarget = {};
const proxy = new Proxy(myTarget, {
  get(target, property, receiver) {
    console.log("get()");
    return Reflect.get(...arguments);
  },
});
proxy.foo; // 触发get()捕获器

返回值 返回值无限制。

拦截的操作

  • proxy.property
  • proxy[property]
  • Object.create(proxy)[property]
  • Reflect.get(proxy, property, receiver)

捕获器处理程序参数

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。① - receiver:代理对象或继承代理对象的对象。

捕获器不变式 如果 target.property 不可写且不可配置,则处理程序返回的值必须与 target.property 匹配。 如果 target.property 不可配置且[[Get]]特性为 undefined,处理程序的返回值也必须是 undefined。

4.2 set()

set()捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。

const myTarget = {};
const proxy = new Proxy(myTarget, {
  set(target, property, value, receiver) {
    console.log("set()");
    return Reflect.set(...arguments);
  },
});
proxy.foo = "bar"; // 触发set()捕获器

返回值 返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。

拦截的操作

  • proxy.property = value
  • proxy[property] = value
  • Object.create(proxy)[property] = value
  • Reflect.set(proxy, property, value, receiver)

捕获器处理程序参数

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。
  • value:要赋给属性的值。
  • receiver:接收最初赋值的对象。

捕获器不变式 如果 target.property 不可写且不可配置,则不能修改目标属性的值。 如果 target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性的值。 在严格模式下,处理程序中返回 false 会抛出 TypeError。

4.3 has()

has()捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。

const myTarget = {};
const proxy = new Proxy(myTarget, {
  has(target, property) {
    console.log("has()");
    return Reflect.has(...arguments);
  },
});
"foo" in proxy; //触发 has()捕获器

返回值 has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。

拦截的操作

  • property in proxy
  • property in Object.create(proxy)
  • with(proxy) {(property);}
  • Reflect.has(proxy, property)

捕获器处理程序参数

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。

捕获器不变式 如果 target.property 存在且不可配置,则处理程序必须返回 true。 如果 target.property 存在且目标对象不可扩展,则处理程序必须返回 true。

4.4 deleteProperty()

deleteProperty()捕获器会在 delete 操作符中被调用。对应的反射 API 方法为 Reflect.deleteProperty()。

const myTarget = {};
const proxy = new Proxy(myTarget, {
  deleteProperty(target, property) {
    console.log("deleteProperty()");
    return Reflect.deleteProperty(...arguments);
  },
});
delete proxy.foo; // 触发deleteProperty()捕获器
  • 返回值 deleteProperty()必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。
  • 拦截的操作
  • delete proxy.property
  • delete proxy[property]
  • Reflect.deleteProperty(proxy, property)
  • 捕获器处理程序参数
  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。
  • 捕获器不变式 如果自有的 target.property 存在且不可配置,则处理程序不能删除这个属性。

4.5 apply()

apply()捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply()。

const myTarget = () => {};
const proxy = new Proxy(myTarget, {
  apply(target, thisArg, ...argumentsList) {
    console.log("apply()");
    return Reflect.apply(...arguments);
  },
});
proxy(); // 触发apply()捕获器
  • 返回值 返回值无限制。
  • 拦截的操作
  • proxy(...argumentsList)
  • Function.prototype.apply(thisArg, argumentsList)
  • Function.prototype.call(thisArg, ...argumentsList)
  • Reflect.apply(target, thisArgument, argumentsList)
  • 捕获器处理程序参数
  • target:目标对象。
  • thisArg:调用函数时的 this 参数。
  • argumentsList:调用函数时的参数列表
  • 捕获器不变式 target 必须是一个函数对象。

4.6 construct()

construct()捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()。

const myTarget = function () {};
const proxy = new Proxy(myTarget, {
  construct(target, argumentsList, newTarget) {
    console.log("construct()");
    return Reflect.construct(...arguments);
  },
});
new proxy(); // 触发construct()捕获器
  • 返回值 construct()必须返回一个对象。
  • 拦截的操作
  • new proxy(...argumentsList)
  • Reflect.construct(target, argumentsList, newTarget)
  • 捕获器处理程序参数
  • target:目标构造函数
  • argumentsList:传给目标构造函数的参数列表。
  • newTarget:最初被调用的构造函数。
  • 捕获器不变式 target 必须可以用作构造函数。

还有另外七种捕获器:

  • defineProperty()捕获器会在 Object.defineProperty()中被调用。
  • getOwnPropertyDescriptor()捕获器会在 Object.getOwnPropertyDescriptor()中被调 用。
  • ownKeys()捕获器会在 Object.keys()及类似方法中被调用。
  • getPrototypeOf()捕获器会在 Object.getPrototypeOf()中被调用。
  • setPrototypeOf()捕获器会在 Object.setPrototypeOf()中被调用。
  • isExtensible()捕获器会在 Object.isExtensible()中被调用。
  • preventExtensions()捕获器会在 Object.preventExtensions()中被调用。

这七种捕获器详细介绍可参考MDN - Proxy

参考博客

《Javascript 高级程序设计(第 4 版)》(JS 红宝书)

MDN - Proxy

MDN - Reflect