在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 方法有两件事要做:
- 首先,将正在检索的对象键进行记录。
- 因为我们仍然希望对象正常工作,所以我们使用
Reflect API从目标的"内部槽"中获取属性值,然后从“陷阱”中返回。
所有对象都将数据存储在内部插槽中,这些插槽无法直接从代码中访问。…
Reflect API提供了一种方法来调用能够与对象的内部槽进行交互的内部运行时方法。
上面的打印为:
Access: "introduceYourselfTo"
Access: "fullName"
Hello jack! My name is leo lau.
在使用 proxy 时,重要的是要记住javascript对象是如何工作的细节。当调用方法时,必须首先调用对象上的 get 方法。这就是为什么我们看到第一个日志语句显示 Access: "introduceYourselfTo"。然后,当该方法应用于 proxy 时,运行时将调用get方法获取fullName。
但是为什么没有打印出 firstName 和 lastName 呢,毕竟我们在访问 fullName的时候内部是访问了firstName 和 lastName 的。
要理解这一点,就需要深入了解在 javascript 运行时发生的事情。
在上面的代码中,introduceYourselfTo 方法是通过在 Proxy 的 get 方法中检索的,调用 proxy.introduceYourselfTo("jack") 方法,此时上下文 this 指向 proxy 对象,运行时通过 proxy 对象获取到 fullName,此时就再一次触发 proxy中的 get 方法并打印 Access: "fullName"。这里就是它变得有趣的地方。
当我们使用 Reflect.get(target, property) 运行时将访问内部的 fullName。因为fullName 是一个属性,它会调用在属性描述符上设置的get方法。此时 fullName中的 this 是属于 target 而不是 proxy。所以我们在proxy中设置的拦截方法无法拦截 firstName 和 lastName。
所以,如果我们想拦截所有的东西怎么解决?我们的第一个想法可能是把 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去调用的话就会导致错误)
由于这些原因,在使用代理或将对象传递给使用代理的其他库时,我们需要非常小心。