深入理解Proxy和Reflect
说明
代理和反射笔记整理
# Proxy代理
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。由它来“代理”某些操作,代理应该在所有地方都完全替代目标对象。目标对象被代理后,任何人都不应该再引用目标对象。否则很容易搞砸
所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy对象可以传入两个参数,分别是需要被Proxy
代理的对象和一系列的捕获器
语法: let proxy = new Proxy(target, handler)
target
—— 是要包装的对象,可以是任何东西,包括函数。handler
—— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如get
捕捉器用于读取target
的属性,set
捕捉器用于写入target
的属性,等等。
let target = {}
let proxy = new Proxy(target, {}) // 空的 handler 对象
proxy.test1 = 5 // 写入 proxy 对象
proxy.test2 = 6
proxy.test3 = 7
alert(target.test1) // 5,test属性出现在了target中
alert(proxy.test1) // 5,我们也可以从 proxy 对象读取它
for (let key in proxy) alert(key) // test1 test2 test3, 迭代也正常工作
// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
2
3
4
5
6
7
8
9
10
11
12
13
14
由于没有捕捉器,所有对 proxy
的操作都直接转发给了 target
。
- 写入操作
proxy.test
会将值写入target
。 - 读取操作
proxy.test
会从target
返回对应的值。 - 迭代
proxy
会从target
返回对应的值。
# 代理捕捉器
![image-20230320162403181](/assets/img/image-20230320162403181.3966fd32.png)
# get捕捉器
get(target, property, receiver)方法。
读取属性时触发该方法,参数如下:
target
—— 是目标对象,该对象被作为第一个参数传递给new Proxy
property
—— 目标属性名receiver
—— 如果目标属性是一个 getter 访问器属性,则receiver
就是本次读取属性所在的this
对象。通常,这就是proxy
对象本身(或者,如果我们从 proxy 继承,则是从该 proxy 继承的对象)。现在我们不需要此参数,因此稍后我们将对其进行详细介绍。
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // 拦截读取属性操作
if (phrase in target) { //如果词典中有该短语
return target[phrase]; // 返回其翻译
} else {
// 否则返回未翻译的短语
return phrase;
}
}
});
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome']); // Welcome(没有被翻译)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# set捕捉器
当写入属性时 set
捕捉器被触发。
set(target, property, value, receiver)
:
target
—— 是目标对象,该对象被作为第一个参数传递给new Proxy
,property
—— 目标属性名称,value
—— 目标属性的值,receiver
—— 与get
捕捉器类似,仅与 setter 访问器属性相关。
如果写入操作(setting)成功,set
捕捉器应该返回 true
,否则返回 false
(触发 TypeError
)。
let numbers = [];
numbers = new Proxy(numbers, {
set(target, prop, val) {
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
try {
numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)
} catch (error) {
console.log(error);
}
alert("Length is: " + numbers.length); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 迭代处理
使用 ownKeys
捕捉器拦截 for..in
的遍历操作
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 过滤掉了 _password
for(let key in user) alert(key); // name,然后是 age
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.keys
仅返回带有 enumerable
标志的属性。为了检查它,该方法会对每个属性调用内部方法 [[GetOwnProperty]]
来获取它的描述符descriptor。
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
2
3
4
5
6
7
8
9
在这里,由于没有属性,其描述符为空,没有 enumerable
标志,因此它被略过。我们可以拦截对 [[GetOwnProperty]]
的调用(捕捉器 getOwnPropertyDescriptor
可以做到这一点),并返回带有 enumerable: true
的描述符
let user = {};
user = new Proxy(user, {
ownKeys(target) { // 一旦要获取属性列表就会被调用
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 被每个属性调用
return {
enumerable: true,
configurable: true
/* ...其他标志,可能是 "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# has捕捉器
has捕捉器会拦截 in调用。has(target, property)
target
—— 是目标对象,被作为第一个参数传递给new Proxy
,property
—— 属性名称。
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
2
3
4
5
6
7
8
9
10
11
12
13
# 多个Proxy捕捉器
const obj = {
name: "qwe",
age:18,
id: 1
}
const objProxy = new Proxy(obj,{
get:function(target,key){
console.log(`get key:${key}`);
return target[key]
},
set:function(target,key,val){
console.log(`set key:${key}`);
target[key] = val
}
})
console.log(objProxy.name); // get key:name qwe
objProxy.name = "rio" // set key:name
objProxy.address = 'cn' // set key:address
console.log(objProxy.address); // get key:address cn
-------------------------------------------------------
const objProxy = new Proxy(obj,{
get:function(target,key){
console.log(`get key:${key}`);
return target[key]
},
set:function(target,key,val){
console.log(`set key:${key}`);
target[key] = val
},
deleteProperty:function(target,key){
console.log(`del:${key}属性`);
delete obj[key]
}
})
delete objProxy.id // del:id属性
console.log(objProxy); // Proxy {name: 'qwe', age: 18}
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
# Proxy其他捕获器
function foo(a,b) {
console.log(this,a,b);
}
const fooProxy = new Proxy(foo,{
apply:function(target,thisArg,others){
console.log(`apply target:${target}`);
target.apply(thisArg,others)
},
construct:function(target, args){
console.log("proxy new");
console.log(target,args);
return new target(...args)
}
})
foo.apply("aaa",[111,222]) // String {'aaa'} 111 222
new fooProxy('a','b')
// proxy new
// ƒ foo(a,b) {
// console.log(this,a,b);
// } (2) ['a', 'b']
// foo {} 'a' 'b'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 捕获器不变式
捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为,比如如果目标对象有一个不可配置且不可写的数据属性那么在捕获器返回一个与该属性不同的值时会抛出TypeError
const target = {};
Object.defineProperty(target, 'foo', {
configurable: false,
writable: false,
value: 'bar'
});
const handler = {
get() {
return 'qux';
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo);
// TypeError
2
3
4
5
6
7
8
9
10
11
12
13
14
# 可撤销代理
revocable()
方法支持撤销代理对象与目标对象的关联,撤销代理的操作是不可逆的。
语法为 let {proxy, revoke} = Proxy.revocable(target, handler)
撤销函数和代理对象是在实例化过程中同时生成的
const target = {
foo: 'bar'
};
const handler = {
get() {
return 'intercepted';
}
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError
2
3
4
5
6
7
8
9
10
11
12
13
# Reflect
Reflect是ES6
新增的一个API
,它是一个对象,字面的意思是反射。它主要提供了很多操作JavaScript对象的方法,还可简化 Proxy
的创建。
const obj = {
name: 'rio'
}
obj.name
// 等同于
Reflect.get(obj, 'name')
2
3
4
5
6
7
const p1 = {
name: 'rio1',
age: 20,
get msg() {
return this.name;
}
}
const p2 = {
name: 'rio2',
age: 22,
get msg() {
return this.name;
}
}
console.log(p1.msg); // rio1
console.log(Reflect.get(p1, 'msg')); // rio1
console.log(Reflect.get(p1, 'msg', p2)); // rio2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对于每个可被 Proxy
捕获的内部方法,在 Reflect
中都有一个对应的方法,其名称和参数与 Proxy
捕捉器相同。开发者并不需要手动重建原始行为,而是可以调用全局 Reflect 对象上(封装了原始行为)的同名方法来轻松重建,原始行为就是例如Proxy使用了get捕捉器,需要实现get功能可以手动return target[props]
也可以使用Reflect的同名方法快速重建原始行为
例如 [[Get]]
和 [[Set]]
等,都只是规范性的,不能直接调用。Reflect
对象使调用这些内部方法成为了可能。
const target = {
foo: 'bar'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
--------------------------------------
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
2
3
4
5
6
7
8
9
10
11
12
13
14
15
优势:
有返回值可以判断本次操作是否成功
const obj = { name: 'rio', age: 18 } if (Reflect.deleteProperty(obj,"name")) { console.log("success"); } else { console.log("fail"); } console.log(obj); //success //{age: 18}
1
2
3
4
5
6
7
8
9
10
11
12
13
14可以设置receiver改变原对象set get的this指向
读取属性的
Reflect.get(target, prop, receiver)
可以被替换为target[prop]
但有一些细微差别let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; // (*) target = user } }); let admin = { __proto__: userProxy, _name: "Admin"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
};
alert(admin.name); // 输出:Guest
1. 当我们读取 `admin.name` 时,由于 `admin` 对象自身没有对应的的属性,搜索将转到其原型 `userProxy`。
2. 从代理读取 `name` 属性时,`get` 捕捉器会被触发,并从原始对象返回 `target[prop]` 属性,在 `(*)` 行。
当调用 `target[prop]` 时,若 `prop` 是一个 getter,它将在 `this=target` 上下文中运行其代码。因此,结果是来自原始对象 `target` 的 `this._name`,即来自 `user`。
为了解决这种情况,我们需要 `get` 捕捉器的第三个参数 `receiver`。它保证将正确的 `this` 传递给 getter。在我们的例子中是 `admin`。
如何把上下文传递给 getter?对于一个常规函数,我们可以使用 `call/apply`,但这是一个 getter,它不能“被调用”,只能被访问。
**Reflect.get**
```js
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
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
现在 receiver
保留了对正确 this
的引用(即 admin
),该引用是在 (*)
行中被通过 Reflect.get
传递给 getter 的。
# Proxy的局限性
this指向
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
2
3
4
5
6
7
8
9
this指向引起的无法代理问题
const p1 = {
name: 'rio1',
age: 20,
get msg() {
return this.name;
}
}
const proxy = new Proxy(p1,{
get(target,key,receiver){
console.log("get触发");
return target[key] // 有问题的写法
// Reflect.get(target,key,receiver) 正确的写法
}
});
console.log(proxy.msg); // rio1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
理论上get触发
应该打印两次即proxy.msg
监听一次 get msg()中return this.name
监听一次 但因为在proxy中return的 target[key]
中的get msg的this指向不是proxy而是p1从而出现了this指向的监听问题
# Proxy与Reflect 共同使用
const obj = {
name: 'rio',
age: 18
}
const objProxy = new Proxy(obj,{
set:function(target,key,val,receiver){
const isSuccess = Reflect.set(target,key,val,receiver)
if (!isSuccess) {
throw new Error(`set ${key} error`)
}else{
console.log("set success");
console.log(receiver);
}
},
get:function(target,key,receiver){
console.log(target,key,receiver);
return Reflect.get(target,key,receiver)
}
})
console.log(objProxy.name);
// {name: 'rio', age: 18} 'name' Proxy {name: 'rio', age: 18}
// rio
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 内部插槽
许多内建对象,例如 Map
,Set
,Date
,Promise
等,都使用了所谓的“内部插槽”。
它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map
将项目(item)存储在 [[MapData]]
中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]]
内部方法。所以 Proxy
无法拦截它们。
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
2
3
Map
将所有数据存储在其 [[MapData]]
内部插槽中。代理对象没有这样的插槽。内建方法 Map.prototype.set
(opens new window) 方法试图访问内部属性 this.[[MapData]]
,但由于 this=proxy
,在 proxy
中无法找到它,只能失败。
解决办法
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
2
3
4
5
6
7
8
9
10
11
它正常工作了,因为 get
捕捉器将函数属性(例如 map.set
)绑定到了目标对象(map
)本身。
与前面的示例不同,proxy.set(...)
内部 this
的值并不是 proxy
,而是原始的 map
。因此,当set
捕捉器的内部实现尝试访问 this.[[MapData]]
内部插槽时,它会成功。
私有字段
私有字段是通过内部插槽实现的。JavaScript 在访问它们时不使用 [[Get]]/[[Set]]
。
在调用 getName()
时,this
的值是代理后的 user
,它没有带有私有字段的插槽
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
2
3
4
5
6
7
8
9
10
11
12
13
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 参考
《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版 (opens new window)
现代 JavaScript 教程 (opens new window)
JavaScript高级程序设计(第4版)