探秘 Proxy 和 Reflect

简介: Proxy 是一个构造函数,接收两个参数:原对象和捕捉器。Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法


Proxy 是一个构造函数,接收两个参数:原对象和捕捉器。Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

const p = new Proxy(target, handler)

target: 任何类型的对象

handler:捕捉器,可以代理捕获 13 种操作(具体种类见附录),所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

先来看一个最简单的示例

const object = {
  name: 'king',
  age: 18,
};
const proxy = new Proxy(object, {
  get(target, key, receiver) {
    return target[key];
  },
  set(target, key, newValue, receiver) {
    console.log(key, 'setting new value:', newValue);
    target[key] = newValue;
    return true; // set 捕获器需要返回 boolean 表示操作是否成功
  },
});
console.log(proxy.name);
proxy.name = 'queen';
console.log(proxy.name);
// king
// name setting new value: queen
// queen
复制代码

大名鼎鼎的 Vue3 核心原理就是基于 Proxy 来实现的,在 get 捕获器中收集依赖,在 set 捕获器中触发依赖,具体原理就不在此赘述了。

但是其实这不是最规范的用法,规范的用法应该是配合反射来完成代理

const proxy = new Proxy(object, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    console.log(key, 'setting new value:', newValue);
    return Reflect.set(target, key, newValue, receiver);
  },
});
复制代码

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。Reflect 提供了13 种静态方法,对应 Proxy中 handler 的 13 中捕获器。


捕获器不变式


捕获器拥有改变所有基本方法的能力,但不是没有限制。即捕获器不能违反属性描述,例如

const o = {};
Object.defineProperty(o, 'name', {
  value: 'queen',
  writable: false,
});
const p = new Proxy(o, {
  get() {
    return 'king';
  },
});
console.log(p.name);
复制代码

这段代码首先定义了一个空对象,然后使用 Object.defineProperty 定义了一个 name 属性并且设置了值不可写,即这个值是只读的,但是在 proxy 中却返回了不一样的值,这就会抛出 TypeError

1682518910(1).png


receiver


有的人可能已经注意到了上面的示例中get 和 set 都有一个 receiver 参数,这个参数一般不会使用,但在某些情况下,他会发挥很大的作用。它的指向是Proxy 或者继承 Proxy 的对象

在一般情况下,receiver 指向的是 Proxy 对象

const objRec = {
  name: 'king',
};
const pRec = new Proxy(objRec, {
  get(target, key, receiver) {
    if (receiver === pRec) {
      console.log('receiver is proxy object.');
    }
    return Reflect.get(target, key);
  },
});
console.log(pRec.name);
// receiver is proxy object.
// king
复制代码

但是如果有个对象继承了这个 proxy,就会是另一种情况

const objRec = {
  name: 'king',
};
const objExt = {} as { name: string };
const pRec = new Proxy(objRec, {
  get(target, key, receiver) {
    if (receiver === pRec) {
      console.log('receiver is proxy object.');
    }
    if (receiver === objExt) {
      console.log('receiver is extends object.');
    }
    return Reflect.get(target, key);
  },
});
console.log(pRec.name);
Object.setPrototypeOf(objExt, pRec);
console.log(objExt.name);
// receiver is proxy object.
// king
// receiver is extends object.
// king
复制代码

可以看到当有对象继承自 proxy 对象时,这个对象访问自身不存在的属性时,就会访问到 proxy 对象,此时 receiver 就指向就不再是 proxy对象了。

上面看完了 proxy 的 receiver,下面再来看一下 Reflect 的 receiver

如果target对象中指定了getterreceiver则为getter调用时的this值。

上面是 MDN 中对 Reflect.get 的 receiver 描述,可能会有点绕,那我们从例子中来理解一下

const obj = {
  name: 'king',
  get value() {
    return this.name;
  },
};
const objExt = { name: 'queen' } as { value: string; name: string };
const p = new Proxy(obj, {
  get(target, key) {
    return Reflect.get(target, key);
  },
});
console.log(p.value); // king 
Object.setPrototypeOf(objExt, p);
console.log(objExt.value); // king
复制代码

第一个 king 不难理解,p.value,因为 p 是 obj 的代理对象,所以获取到了obj 的 value 值。按照 this 的指向,第二个值是 objExt 调用,按照 this 的指向,这里的值应该是 queen。

这里可以看这段代码

const a = {
  name: 'zhang',
  get value() {
    return this.name;
  },
};
const b = { name: 'li' } as { value: string; name: string };
Object.setPrototypeOf(b, a);
console.log(b.value); // li
复制代码

回到之前的问题,为什么第二个打印的结果是 king 而不是 queue,这里其实是因为 Proxy 的 get 的值始终返回的是 target[key],而 target 是固定的,所以 this 的指向就在这里被偷偷改变了。

要解决这个问题就需要再 Reflect 的参数中加入 receiver,再回味一下之前的那局话

如果target对象中指定了getter,receiver则为getter调用时的this值。

const objRec = {
  name: 'king',
  get value() {
    return this.name;
  },
};
const objExt = { name: 'queen' } as { value: string; name: string };
const pRec = new Proxy(objRec, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});
console.log(pRec.value); // king
Object.setPrototypeOf(objExt, pRec);
console.log(objExt.value); // queen
复制代码

此时的结果就是正常的了,所以,当有需要使用的 this 计算值的时候,最好在反射 API 中传入 receiver


可撤销的代理


Proxy 还提供了一个静态方法Proxy.revocable()用于创建可撤销的代理对象。

Proxy.revocable(target, handler)

const o = { name: 'king' };
const p = Proxy.revocable(o, {
  get(target, key) {
    return Reflect.get(target, key);
  },
});
console.log(p);
console.log(p.proxy.name);
p.revoke();
console.log(p);
复制代码

这里的 p 不再是一个 proxy 对象,而是在外面又包了一层,除了 proxy 对象还提供了一个取消方法,调用该方法之后,proxy 对象将被销毁

1682518952(1).png


附:所有的捕获器类型


  • get(target, property, receiver):拦截对象的读取属性操作
  • set(target, property, value, receiver):拦截设置属性值操作
  • construct(target, argumentsList, newTarget):拦截 new 操作符
  • has(target, prop):拦截 in 操作
  • apply(target, thisArg, argumentsList):拦截函数的调用
  • defineProperty(target, property, descriptor):拦截对象的 defineProperty 操作
  • deleteProperty(target, property):拦截对象的 delete 操作
  • getOwnPropertyDescription(target, prop):拦截Object.getOwnPropertyDescriptor()
  • getPrototyprOf(target):拦截读取代理对象的原型的操作
  • setPropertyOf(target, prototype):拦截 Object.setPrototypeOf()
  • isExtensible(target):拦截对对象的 Object.isExtensible()
  • ownKeys(target):拦截对象的keys 迭代
  • preventExtensions(target):拦截对Object.preventExtensions()



相关文章
|
前端开发 JavaScript
Tailwind CSS:基础使用/vue3+ts+Tailwind
Tailwind CSS:基础使用/vue3+ts+Tailwind
830 0
|
域名解析 Kubernetes 前端开发
开源项目:jeecg-boot低代码平台部署到kubernetes(更新于2022.2.15)
开源项目:jeecg-boot低代码平台部署到kubernetes(更新于2022.2.15)
700 0
开源项目:jeecg-boot低代码平台部署到kubernetes(更新于2022.2.15)
|
6月前
|
设计模式 Java API
Java 高效开发实战之让代码质量飙升的 10 个黄金法则技巧
本文分享了10个提升Java代码质量的黄金法则,涵盖日志优化、集合操作、异常处理、资源管理等方面。通过参数化日志减少性能开销,利用Stream与Guava简化集合操作,采用CompletableFuture优化并发处理,运用Optional避免空指针异常等实战技巧,结合具体案例解析,助你写出高效、高质量的Java代码。
238 1
|
8月前
|
中间件 PHP
在ThinkPHP框架中解决跨域问题的三种方法
以上就是在ThinkPHP框架中解决跨域问题的三种方法。希望这些方法能帮助你解决你的问题。
579 11
|
8月前
|
存储 JavaScript 前端开发
|
4月前
|
存储 供应链 前端开发
如何开发供应商管理系统中的物料管理板块(附架构图+流程图+代码参考)
供应商管理系统中的物料管理板块是企业供应链管理的核心部分,涉及物料采购、库存、资质及价格管理。它通过标准化、自动化管理,降低采购成本,提高供应链透明度与合规性,确保企业物资供应稳定、高效。本文详细解析物料管理的架构设计、核心功能与业务流程,并提供代码示例与开发技巧,助力企业快速构建高效物料管理系统。
|
9月前
|
缓存 自然语言处理 JavaScript
JavaScript中闭包详解+举例,闭包的各种实践场景:高级技巧与实用指南
闭包是JavaScript中不可或缺的部分,它不仅可以增强代码的可维护性,还能在模块化、回调处理等场景中发挥巨大作用。然而,闭包的强大也意味着需要谨慎使用,避免潜在的性能问题和内存泄漏。通过对闭包原理的深入理解以及在实际项目中的灵活应用,你将能够更加高效地编写出简洁且功能强大的代码。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11月前
|
存储 弹性计算 运维
Hologres计算组实例&分时弹性入门实践
本文整理自 Hologres 产品团队的观秋老师关于Hologres 计算组实例&分时弹性入门实践的分享。内容主要为以下三部分: 1. Hologres 计算组实例介绍 2. 计算组实例入门实践 3. 分时弹性入门实践
290 16
|
前端开发 JavaScript 安全
在vue前端开发中基于refreshToken和axios拦截器实现token的无感刷新
在vue前端开发中基于refreshToken和axios拦截器实现token的无感刷新
1503 4
|
小程序 安全 搜索推荐
【微信小程序开发实战项目】——如何制作一个属于自己的花店微信小程序(3)
这是一篇关于微信小程序开发的文章摘要,作者介绍了如何创建一个网上花店小程序,旨在提供便捷的购花体验。小程序包含鲜花分类功能,允许用户按品种、颜色和用途筛选,确保快速找到合适的鲜花。它还提供了配送服务,保证鲜花的新鲜度。文章展示了`cash.wxml`、`cash.wxss`和`cash.js`的部分代码,用于实现分类和商品展示,以及`qin.wxml`、`qin.wxss`和`qin.js`,涉及商品详情和购买付款流程。代码示例展示了商品列表渲染和交互逻辑,包括页面跳转、数据传递和点击事件处理。文章最后提到了购买付款界面,强调了安全和便捷的支付体验。
497 0
【微信小程序开发实战项目】——如何制作一个属于自己的花店微信小程序(3)