0%

ES6 Proxy 对象特性和存在的缺陷

ES6 Proxy 对象特性和存在的缺陷

详细介绍 ES6 中新加入的 Proxy 对象的特性和在使用中存在的缺陷

Proxy 实例

首先看看 Proxy 是如何代理一个对象的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
constructor(first, last) {
this.firstName = first;
this.lastName = last;
}

get fullName() {
return `${this.firstName} ${this.lastName}`;
}

introduce(other = "friend") {
console.log(`Hello ${other}, my name is ${this.fullName}!`);
}
}

const leo = new Person("Leo", "Messi");

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

proxy.introduce();

上面的代码实例化了一个 Proxy 对象, 传递一个 Person 对象来作为被代理的对象, 这时, 调用 proxy 的 get 方法要做两件事:

  1. 将正在检索的对象键打印出来
  2. 使用 Reflect.get 将属性值从实例化的 Person 对象中取出来

上面的打印结果是:

1
2
3
Access: introduce
Access: fullName
Hello friend, my name is Leo Messi!
  1. 在 js 的对象中,调用一个方法时, 必然先调用对象上的 get 方法, 所以第一条打印信息是 introduce
  2. 此时, introduce 方法中的 this 指向 proxy 对象, 所以调用 this.fullName 会再次调用 proxy 的 get 方法打印第二条信息
  3. 最后打印 introduce 方法的返回值

但为什么没有打印 firstName 和 lastName 呢, 当访问 fullName 时内部确实也访问了这两个变量的

在我们使用 Reflect.get 访问内部的 fullName 时, 因为 fullName 是一个属性, 它会调用属性描述符上的 get 方法, 此时的 this 在运行时已经指向了 target, 而 target 不是 proxy, 不会触发 proxy 中定义的 get 方法

为了完成 person 对象的全面代理, 则需要设置 Reflect 的第三个参数:

1
2
3
4
5
6
7
8
9
10
...

const proxy = new Proxy(leo, {
get(target, prop, receiver) {
console.log(`Access: ${prop}`);
return Reflect.get(target, prop, receiver);
},
});

proxy.introduce();

这个操作会将使用 Reflect.get 获取 taget 内部属性的 this 也指向调用者, 而调用者则是 proxy

打印结果如下:

1
2
3
4
5
Access: introduce
Access: fullName
Access: firstName
Access: lastName
Hello friend, my name is Leo Messi!

Proxy 撤销

通过 Proxy.revocable 方法创建的代理对象是可撤销代理的对象, 这种代理可以被创建者禁用, 下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
...

const { proxy, revoke } = Proxy.revocable(leo, {
get(target, prop, receiver) {
console.log(`Access: ${prop}`);
return Reflect.get(target, prop, receiver);
},
});

proxy.introduce();
revoke()
proxy.introduce("boy");

其结果为:

1
2
3
4
5
6
Access: introduce
Access: fullName
Access: firstName
Access: lastName
Hello friend, my name is Leo Messi!
Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

Proxy.revocable 会返回一个 revoke 方法, 调用后就可以撤销代理

Proxy 的缺陷

兼容性

作为 ES6 的特性, Proxy 对于旧版浏览器支持不好, 即使使用 proxy-polyfill 也仅能部分支持 Proxy 的功能

在 github 上的 GoogleChrome/proxy-polyfill 库目前只支持以下 traps:

  • get
  • set
  • apply
  • constructor

性能

Proxy 的性能比 Promise 还差, 也差于 Object.defineProperty

不能安全代理私有成员

修改之前的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person {
#firstName;
#lastName;

constructor(first, last) {
this.#firstName = first;
this.#lastName = last;
}

get firstName() {
return this.#firstName;
}

get lastName() {
return this.#lastName;
}

get fullName() {
return `${this.firstName} ${this.lastName}`;
}

introduce(other = "friend") {
console.log(`Hello ${other}, my name is ${this.fullName}!`);
}
}

const leo = new Person("Leo", "Messi");

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

proxy.introduce();

现在看看打印结果:

1
2
3
Access: introduce
Access: fullName
Hello friend, my name is Leo Messi!

貌似没有问题, 但如果给 Reflect 传入第三个参数, 则会出现问题:

1
2
3
4
5
6
7
8
9
10
...

const proxy = new Proxy(leo, {
get(target, prop, receiver) {
console.log(`Access: ${prop}`);
return Reflect.get(target, prop, receiver);
},
});

proxy.introduce();

打印结果为:

1
2
3
4
Access: introduce
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 时会指向一个私有变量, 私有变量不能在对象外获取, 则会出现这个问题

如果方法中使用了私有成员, 如:

1
2
3
4
...
introduce(other = "friend") {
console.log(`Hello ${other}, my name is ${this.#firstName} ${this.#lastName}!`);
}

则可以在 Proxy 的 getter 中再次改变 this 指向来用原有对象调用该方法:

1
2
3
4
5
6
7
8
9
10
11
...

const proxy = new Proxy(leo, {
get(target, prop, receiver) {
console.log(`Access: ${prop}`);
const value = Reflect.get(target, prop, receiver);
return typeof value === 'function' ? value.bind(target) : value
},
});

proxy.introduce();

打印结果为:

1
2
Access: introduce
Hello friend, my name is Leo Messi!

这样就能在调用使用了私有成员的方法时获取到正确的结果了