在js中使用proxy的棘手问题

2023-10-04 22:15
文章标签 问题 使用 js proxy 棘手

本文主要是介绍在js中使用proxy的棘手问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在js中使用proxy的棘手问题

ES2015引入了大量的新功能,其中一个特性是Proxy(查看proxy详细介绍与使用)。虽然proxy能代来非常多好处,但是它具有一些限制。有人会称之为"设计缺陷"。在这篇文章里,我们就来看看一些棘手的问题。

proxy实例

让我创建一个简单的proxy实例,了解平台如何工作的最简单方法是从记录与底层目标的交互引用开始。对于我们的例子,我们将使用一个简单的实例 Person 作为我们的代理目标。

// person.js
export class Person {constructor(firstName, lastName) {this.firstName = firstName;this.lastName = lastName;}get fullName() {return `${this.firstName} ${this.lastName}`;}introduceYourselfTo(other = "friend") {console.log(`Hello ${other}! My name is ${this.fullName}.`);}
}

让我们创建一个基本的 proxy 来拦截所有的属性访问并将其打印到控制台。

// person-proxy.js
import { Person } from "./person.js";const leo = new Person("leo", "lau");const proxy = new Proxy(leo, {get(target, property) {console.log(`Access: "${property}"`);return Reflect.get(target, property);},
});proxy.introduceYourselfTo("jack");

上面的代码实例化了一个 Proxy 对象,传递了一个 Person 对象来作为要代理的对象,同时设置了一些 ”陷阱“ 配置。

这些"陷阱"是与运行时挂钩,可以让我们拦截与目标的交互。在上面的例子中,我们的 get 方法有两件事要做:

  1. 首先,将正在检索的对象键进行记录。
  2. 因为我们仍然希望对象正常工作,所以我们使用 Reflect API从目标的"内部槽"中获取属性值,然后从“陷阱”中返回。

所有对象都将数据存储在内部插槽中,这些插槽无法直接从代码中访问。…Reflect API提供了一种方法来调用能够与对象的内部槽进行交互的内部运行时方法。

上面的打印为:

Access: "introduceYourselfTo"
Access: "fullName"
Hello jack! My name is leo lau.

在使用 proxy 时,重要的是要记住javascript对象是如何工作的细节。当调用方法时,必须首先调用对象上的 get 方法。这就是为什么我们看到第一个日志语句显示 Access: "introduceYourselfTo"。然后,当该方法应用于 proxy 时,运行时将调用get方法获取fullName

但是为什么没有打印出 firstNamelastName 呢,毕竟我们在访问 fullName的时候内部是访问了firstNamelastName 的。

要理解这一点,就需要深入了解在 javascript 运行时发生的事情。

在上面的代码中,introduceYourselfTo 方法是通过在 Proxyget 方法中检索的,调用 proxy.introduceYourselfTo("jack") 方法,此时上下文 this 指向 proxy 对象,运行时通过 proxy 对象获取到 fullName,此时就再一次触发 proxy中的 get 方法并打印 Access: "fullName"。这里就是它变得有趣的地方。

当我们使用 Reflect.get(target, property) 运行时将访问内部的 fullName。因为fullName 是一个属性,它会调用在属性描述符上设置的get方法。此时 fullName中的 this 是属于 target 而不是 proxy。所以我们在proxy中设置的拦截方法无法拦截 firstNamelastName

所以,如果我们想拦截所有的东西怎么解决?我们的第一个想法可能是把 proxy 对象本身传递给 Reflect.get

const proxy = new Proxy(john, {get(target, property) {console.log(`Access: "${property}"`);return Reflect.get(proxy, property);}
});

千万不能这么做,这将导致无限循环

Reflect 将试图通过 proxy 获得属性值,而proxy将再次为相同的属性调用设置的拦截方法,它又将试图通过 proxy 获得属性值。

我们需要的是一种方法来告诉 Reflect 哪个对象可以访问内部插槽。但是,在它从内部插槽检索到属性之后,我们希望用proxy来运行属性的getter方法。

为此,我们需要设置Reflect的第三个参数 receiver

// person-proxy-with-receiver.js
import { Person } from "./person.js";const leo = new Person("leo", "lau");const proxy = new Proxy(leo, {get(target, property, receiver) {console.log(`Access: "${property}"`);return Reflect.get(target, property, receiver);}
});proxy.introduceYourselfTo("jack");

通过这个代码,我们可以看到以下输出:

Access: "introduceYourselfTo"
Access: "fullName"
Access: "firstName"
Access: "lastName"
Hello jack! My name is leo lau.

前面只提到了一个get方法的使用,proxy还可以设置其他非常多的方法,详情可以查看这篇文章。

proxy数据保护

通过Proxy.revocable(...)这个方法可以创建一个可撤销代理的数据。这种类型的代理可以被代理的创建者禁用,这样所有仍然持有引用的对象都将被运行时阻止访问对象。这里是一个可撤销的实例:

const leo = new Person("leo", "lau");const { proxy, revoke } = Proxy.revocable(leo, {get(target, property, receiver) {console.log(`Access: "${property}"`);return Reflect.get(target, property, receiver);}
});proxy.introduceYourselfTo("jack");
revoke();
proxy.introduceYourselfTo("Bad Guy");

执行上面的方法会输出如下内容:

Access: "introduceYourselfTo" 
Access: "fullName" 
Access: "firstName" 
Access: "lastName" 
Hello jack! My name is leo lau.
Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

Proxy.revocable会返回一个revoke方法, 如果把这个方法暴露出去,就可以通过调用revoke方法来访问撤销代理。

proxy 中遇到的问题

不能安全地在具有私有成员的对象上使用代理

改写之前的例子:

class Person {#firstName;#lastName;constructor(firstName, lastName) {this.#firstName = firstName;this.#lastName = lastName;}get firstName() {return this.#firstName;}get lastName() {return this.#lastName;}get fullName() {return `${this.firstName} ${this.lastName}`;}introduceYourselfTo(other = "friend") {console.log(`Hello ${other}! My name is ${this.fullName}.`)}
}

现在我们在proxy中使用:

const leo = new Person("leo", "lau");const proxy = new Proxy(leo, {get(target, property) {console.log(`Access: "${property}"`);return Reflect.get(target, property);}
});proxy.introduceYourselfTo("jack");

输出为:

Access: "introduceYourselfTo" 
Access: "fullName" 
Hello jack! My name is leo lau.

看起来没啥问题,但是如果我们使用了receiver

const leo = new Person("leo", "lau");const proxy = new Proxy(leo, {get(target, property, receiver) {console.log(`Access: "${property}"`);return Reflect.get(target, property, receiver);}
});proxy.introduceYourselfTo("jack");

输出:

Access: "introduceYourselfTo" 
Access: "fullName" 
Access: "firstName" 
Uncaught TypeError: Cannot read private member #firstName 
from an object whose class did not declare it

当我们使用receiver时,firstName中的this指向proxy,这个getter指向一个私有属性,不能通过this获取,因此会出现一个运行时的错误。

对于proxy来说这是一个很大的问题,因为我们不能随意控制和验证实现的对象(任何对象都可以使用私有成员,并根据proxy是如何写入的,与对象的特定内部文件相结合,但是使用proxy去调用的话就会导致错误)

由于这些原因,在使用代理或将对象传递给使用代理的其他库时,我们需要非常小心。

这篇关于在js中使用proxy的棘手问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/2833

相关文章

使用Python开发Markdown兼容公式格式转换工具

《使用Python开发Markdown兼容公式格式转换工具》在技术写作中我们经常遇到公式格式问题,例如MathML无法显示,LaTeX格式错乱等,所以本文我们将使用Python开发Markdown兼容... 目录一、工具背景二、环境配置(Windows 10/11)1. 创建conda环境2. 获取XSLT

Python中Flask模板的使用与高级技巧详解

《Python中Flask模板的使用与高级技巧详解》在Web开发中,直接将HTML代码写在Python文件中会导致诸多问题,Flask内置了Jinja2模板引擎,完美解决了这些问题,下面我们就来看看F... 目录一、模板渲染基础1.1 为什么需要模板引擎1.2 第一个模板渲染示例1.3 模板渲染原理二、模板

浅析如何使用xstream实现javaBean与xml互转

《浅析如何使用xstream实现javaBean与xml互转》XStream是一个用于将Java对象与XML之间进行转换的库,它非常简单易用,下面将详细介绍如何使用XStream实现JavaBean与... 目录1. 引入依赖2. 定义 JavaBean3. JavaBean 转 XML4. XML 转 J

使用Python创建一个功能完整的Windows风格计算器程序

《使用Python创建一个功能完整的Windows风格计算器程序》:本文主要介绍如何使用Python和Tkinter创建一个功能完整的Windows风格计算器程序,包括基本运算、高级科学计算(如三... 目录python实现Windows系统计算器程序(含高级功能)1. 使用Tkinter实现基础计算器2.

MySQL主从同步延迟问题的全面解决方案

《MySQL主从同步延迟问题的全面解决方案》MySQL主从同步延迟是分布式数据库系统中的常见问题,会导致从库读取到过期数据,影响业务一致性,下面我将深入分析延迟原因并提供多层次的解决方案,需要的朋友可... 目录一、同步延迟原因深度分析1.1 主从复制原理回顾1.2 延迟产生的关键环节二、实时监控与诊断方案

在.NET平台使用C#为PDF添加各种类型的表单域的方法

《在.NET平台使用C#为PDF添加各种类型的表单域的方法》在日常办公系统开发中,涉及PDF处理相关的开发时,生成可填写的PDF表单是一种常见需求,与静态PDF不同,带有**表单域的文档支持用户直接在... 目录引言使用 PdfTextBoxField 添加文本输入域使用 PdfComboBoxField

SQLyog中DELIMITER执行存储过程时出现前置缩进问题的解决方法

《SQLyog中DELIMITER执行存储过程时出现前置缩进问题的解决方法》在SQLyog中执行存储过程时出现的前置缩进问题,实际上反映了SQLyog对SQL语句解析的一个特殊行为,本文给大家介绍了详... 目录问题根源正确写法示例永久解决方案为什么命令行不受影响?最佳实践建议问题根源SQLyog的语句分

Git可视化管理工具(SourceTree)使用操作大全经典

《Git可视化管理工具(SourceTree)使用操作大全经典》本文详细介绍了SourceTree作为Git可视化管理工具的常用操作,包括连接远程仓库、添加SSH密钥、克隆仓库、设置默认项目目录、代码... 目录前言:连接Gitee or github,获取代码:在SourceTree中添加SSH密钥:Cl

Python中模块graphviz使用入门

《Python中模块graphviz使用入门》graphviz是一个用于创建和操作图形的Python库,本文主要介绍了Python中模块graphviz使用入门,具有一定的参考价值,感兴趣的可以了解一... 目录1.安装2. 基本用法2.1 输出图像格式2.2 图像style设置2.3 属性2.4 子图和聚

windows和Linux使用命令行计算文件的MD5值

《windows和Linux使用命令行计算文件的MD5值》在Windows和Linux系统中,您可以使用命令行(终端或命令提示符)来计算文件的MD5值,文章介绍了在Windows和Linux/macO... 目录在Windows上:在linux或MACOS上:总结在Windows上:可以使用certuti