JavaScript 中的反射与 Proxy 与 Reflect

JavaScript 中的反射与 Proxy 与 Reflect

反射(Reflect)是指程序运行时访问、检测和修改本身状态和行为的能力。在 ES6 中引入 Reflect 之前,常用 for...in 实现反射,而在引入 Reflect 之后,反射机制在 JavaScript 中得到了更大的延伸与应用。

Reflect

Reflect 是一个全局对象,但与其它全局对象不同,它并不是一个构造函数,所以不能通过 new 来对其进行调用。

而且它所有的属性和方法都是静态的(类似的还有 Math)。

1
2
3
4
5
6
7
8
9
10
// ** 检测一个对象是否有特定的属性
const obj = { A: '1', B: '2' };
Reflect.has(obj, 'A'); // true
Reflect.has(obj. 'C'); // false

// ** 返回对象自身的键 has 与 in 运算符作用相同
Reflect.ownKeys(obj); // ['A', 'B']

// ** 为对象添加新的属性
Reflect.set(obj, 'C', '3'); // true

Proxy

Proxy 基于目标对象创建一个代理对象,可以通过代理对象实现对于基本操作的拦截和自定义。

1
2
3
// target 要代理的对象,可以是任何对象,包括函数
// handler 代理配置,带有扑捉器的对象
const proxy = new Proxy(targe, handler);

对 proxy 对象进行操作,如果 handler 中包含了对应的捕捉器,则扑捉器会拦截这个操作,并对其进行处理。如果没有相应的捕捉器,则操作会被转发到被代理的对象。

对被代理对象的操作和相应的捕捉器名称如下:

被代理对象的操作 捕捉器
Object.getPrototypeOf() handler.getPrototypeOf()
Object.setPrototypeOf() handler.setPrototypeOf()
Object.isExtensible() handler.isExtensible()
Object.preventExtensions() handler.preventExtensions()
Object.getOwnPropertyDescriptor() handler.getOwnPropertyDescriptor()
Object.defineProperty() handler.defineProperty()
in handler.has()
属性读取 handler.get()
属性赋值 handler.set()
delete handler.deleteProperty()
Object.getOwnPropertyNames()Object.getOwnPropertySymbols() handler.ownKeys()
函数调用 handler.apply()
new handler.construct()

不变量

为了确保语言功能和行为的一致性,代理对象的捕捉器的返回是有一定限制的,被称为不变量。

  • set() ,如果设置属性值成功,需要返回 true,否则返回 false

  • deleteProperty(),如果属性删除成功,需要返回 true,否则返回 false

  • getPrototypeOf() 必须返回被代理对象的原型

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
37
38
39
40
41
42
43
44
45
46
47
48
"use strict";

let origin = {
name: 'Mike',
age: 29,
_isMarried: false
};

origin = new Proxy(origin, {
isExtensible: function(obj) {
return Reflect.isExtensible(obj);
},
has: function(obj, prop) {
return Reflect.has(obj, prop);
},
get: function get(obj, prop, receiver) {
if (Reflect.has(obj, prop)) {
if (prop.startsWith('_')) return 'Access denied';
return Reflect.get(obj, prop, receiver);
} else {
return "No ".concat(prop, " find in target");
}
},
set: function set(obj, prop, value, receiver) {
if (Reflect.has(obj, prop)) {
if (prop.startsWith('_')) return 'Access denied';
Reflect.set(obj, prop, value, receiver);
return true;
} else {
return false;
}
}
});

console.log(origin._isMarried); // Access denied
console.log(origin._isMarried = '1'); // Access denied

function constructPoint(numX, numY) {
return [numX, numY];
}

const funcProxy = new Proxy(constructPoint, {
apply: function(obj, thisArg, argumentsList) {
return argumentsList.join('-');
}
});

console.log(funcProxy(1, 2)); // 1-2

对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。

所以,我们可以使用 Reflect 来将操作转发给原始对象。

Proxy 的局限

许多内建对象,例如 MapSetDatePromise 等,都使用了所谓的“内部插槽”。

它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]] 内部方法。所以 Proxy 无法拦截它们。

1
2
3
4
5
6
7
8
9
10
11
let map = new Map();

let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1(工作了!)

类似的,类的私有属性也是通过内部插槽而不是 getset 方法,采用上面的方法依旧有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User {
#name = "Guest";

getName() {
return this.#name;
}
}

let user = new User();

user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});

alert(user.getName()); // Guest
作者

Y2hlbmdsZWk=

发布于

2020-06-18

更新于

2021-09-01

许可协议