# 代理和反射
# Proxy
一个 Proxy 对象包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理它们。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy 被用于了许多库和某些浏览器框架。
# 概述
基本语法
const proxy = new Proxy(target, handler);
target
要使用Proxy
包装的目标对象handler
一个通常以函数作为属性的对象,各函数分别定义了在执行各种操作时代理proxy
的行为
对 proxy 进行操作,如果在 handler 中存在相应的捕捉器,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。
let target = {};
let proxy = new Proxy(target, {}); // 空的 handler 对象
proxy.test = 5; // 写入 proxy 对象 (1)
alert(target.test); // 5,test 属性出现在了 target 中!
alert(proxy.test); // 5,我们也可以从 proxy 对象读取它 (2)
for (let key in proxy) alert(key); // test,迭代也正常工作 (3)
由于没有捕捉器,所有对 proxy 的操作都直接转发给了 target。
- 写入操作 proxy.test= 会将值写入 target。
- 读取操作 proxy.test 会从 target 返回对应的值。
- 迭代 proxy 会从 target 返回对应的值。
# 拦截操作
get(target, propKey, receiver) | 拦截读取 |
set(target, propKey, value, receiver) | 拦截设置 |
has(target, propKey) | 拦截in 操作 |
deleteProperty(target, propKey) | 拦截 delete |
ownKeys(target) | 拦截遍历 |
getOwnPropertyDescriptor(target, propKey) | |
definProperty(target, propKey, propDesc) | |
preventExtensions(target) | |
getPrototypeOf(target) | |
isExtensible(target) | |
setPrototypeOf(target, proto) | |
apply(target, thisBind, args) | 拦截() |
construct(target, args) | 拦截new |
const handler = {
get: (target, propKey, receiver) => {
console.log("proxy get");
if (propKey == "prototype") {
return Object.prototype;
} else {
return "key:" + propKey;
}
},
set: (target, propKey, propValue, receiver) => {
console.log("proxy set");
},
apply: (target, thisBind, args) => {
console.log("proxy apply");
return args[0];
},
construct: (target, args) => {
console.log("proxy new");
return {
value0: args[0],
value1: args[1]
};
}
};
const proxy = new Proxy(function (a, b) {
console.log(a + b);
return a + b;
}, handler);
console.log(proxy(1, 2));
// proxy apply
// 1
console.log(proxy.a);
// key:a
console.log(proxy.prototype);
// {}
console.log((proxy.a = 2));
// proxy set
// 2
let p = new proxy(1, 2);
// proxy new
console.log(p);
// { value0: 1, value1: 2 }
# 带有 “get” 捕捉器的默认值
最常见的捕捉器是用于读取/写入的属性。
要拦截读取操作,handler 应该有 get(target, property, receiver) 方法。
读取属性时触发该方法,参数如下:
- target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy,
- property —— 目标属性名,
- receiver —— 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。
通常,当人们尝试获取不存在的数组项时,他们会得到 undefined,但是我们在这将常规数组包装到代理(proxy)中,以捕获读取操作,并在没有要读取的属性的时返回 0:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默认值
}
}
});
alert(numbers[1]); // 1
alert(numbers[123]); // 0(没有这个数组项)
TIP
代理应该在所有地方都完全替代目标对象。目标对象被代理后,任何人都不应该再引用目标对象。否则很容易搞砸。
# 使用 “set” 捕捉器进行验证
假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误。
let numbers = [];
numbers = new Proxy(numbers, {
// (*)
set(target, prop, val) {
// 拦截写入属性操作
if (typeof val == "number") {
target[prop] = val;
return true; // 请返回true
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError: 'set' on proxy: trap returned falsish
请注意:数组的内建方法依然有效!值被使用 push 方法添加到数组。当值被添加到数组后,数组的 length 属性会自动增加。我们的代理对象 proxy 不会破坏任何东西。
别忘了返回 true
如上所述,要保持不变量。 对于 set 操作,它必须在成功写入时返回 true。 如果我们忘记这样做,或返回任何假(falsy)值,则该操作将触发 TypeError
# 实例: 使用 Proxy 实现观察者模式
// 设置一个观察者集合
const queuedObservers = new Set();
// 添加观察者(方法)函数
const observe = (fn) => queuedObservers.add(fn);
// 添加观察者函数
const observable = (obj) => new Proxy(obj, { set });
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 遍历所有观察者,通知他们(调用他们)
queuedObservers.forEach((observer) => observer());
return result;
}
// 定义一个观察者
function print() {
console.log("观察到了", p.name);
}
// 增加观察者到集合
observe(print);
// 定义一个被观察者
let p = observable({
name: "法外狂徒张三",
age: 20
});
// 修改被观察者(实际上是修改代理对象)
p.name = "李四";
// 观察到了 李四;
p.name = "王五";
// 观察到了 王五;
# Reflect
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。设计目的有如下几个
- 将
Object
对象上的一些明显属于语言内部的方法,放到Reflect
对象上。 - 修改某些
Object
方法的返回结果,让其结果变得更合理。 Reflect
和Proxy
对象的方法一一对应,也就是说只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。
Reflect 是一个内建对象,可简化 Proxy 的创建。
前面所讲过的内部方法,例如 [[Get]]
和 [[Set]]
等,都只是规范性的,不能直接调用。
Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。
let user = {};
Reflect.set(user, "name", "John");
alert(user.name); // John
尤其是,Reflect 允许我们将操作符(new,delete,……)作为函数(Reflect.construct,Reflect.deleteProperty,……)执行调用
所以,我们可以使用 Reflect 来将操作转发给原始对象。
在下面这个示例中,捕捉器 get 和 set 均透明地(好像它们都不存在一样)将读取/写入操作转发到对象,并显示一条消息:
let user = {
name: "John"
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // 显示 "GET name"
user.name = "Pete"; // 显示 "SET name=Pete"
这样,一切都很简单:如果一个捕捉器想要将调用转发给对象,则只需使用相同的参数调用 Reflect.<method>
就足够了。
在大多数情况下,我们可以不使用 Reflect 完成相同的事情,例如,用于读取属性的 Reflect.get(target, prop, receiver) 可以被替换为 target[prop]
。尽管有一些细微的差别。
# 为什么需要 Reflect
让我们看一个示例,来说明为什么 Reflect.get 更好。此外,我们还将看到为什么 get/set 有第三个参数 receiver,而且我们之前从来没有使用过它。
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"
};
// 期望输出:Admin
alert(admin.name); // 输出:Guest (?!?)
发生了什么?或许我们在继承方面做错了什么?
- 当我们读取 admin.name 时,由于 admin 对象自身没有对应的的属性,搜索将转到其原型。
- 原型是 userProxy。
- 从代理读取 name 属性时,get 捕捉器会被触发,并从原始对象返回
target[prop]
属性,在 (*) 行。
当调用 target[prop]
时,若 prop 是一个 getter,它将在 this=target 上下文中运行其代码。因此,结果是来自原始对象 target
的 this._name,即来自 user。
我们可以把捕捉器重写得更短:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect 调用的命名与捕捉器的命名完全相同,并且接受相同的参数。它们是以这种方式专门设计的。
因此,return Reflect... 提供了一个安全的方式,可以轻松地转发操作,并确保我们不会忘记与此相关的任何内容。
# 总结
Proxy 是对象的包装器,将代理上的操作转发到对象,并可以选择捕获其中一些操作。
它可以包装任何类型的对象,包括类和函数。
语法为:
let proxy = new Proxy(target, {
/* trap */
});
……然后,我们应该在所有地方使用 proxy 而不是 target。代理没有自己的属性或方法。如果提供了捕捉器(trap),它将捕获操作,否则会将其转发给 target 对象。
我们还可以将对象多次包装在不同的代理中,并用多个各个方面的功能对其进行装饰。
Reflect API 旨在补充 Proxy。对于任意 Proxy 捕捉器,都有一个带有相同参数的 Reflect 调用。我们应该使用它们将调用转发给目标对象。