ES2015
引入了大量的新功能,其中一个特性是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.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
方法来访问撤销代理。
改写之前的例子:
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
去调用的话就会导致错误)
由于这些原因,在使用代理或将对象传递给使用代理的其他库时,我们需要非常小心。