JavaScript学习笔记(7):代理与反射

代理

定义代理

代理对象是可以用作目标对象的替身,但又完全独立于目标对象的对象。

目标对象既可以直接被操作,也可以通过代理来操作,区别是通过代理进行的操作可以被拦截处理,而直接对目标对象的操作无法在操作前加入自定义的处理过程。

代理是使用Proxy构造函数创建的,构造函数接收两个参数:目标对象和处理程序对象,这两个参数都是必需的。

要创建空代理,可以传一个空的对象字面量作为处理程序对象,从而不对任何操作进行拦截。

要注意,所有对代理的操作几乎都视为直接操作目标对象但加入了附带的操作,对代理进行操作,是很难通过代理的行为看出它是作用于代理而不是直接操作的目标对象,就像在目标对象上可以调用的方法、访问的属性等等,在代理对象上都可以以同样的方式进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建一个对象 obj 和它的代理 proxy
let obj = {
name: "Dasen",
age: 22,
girlfriend: null
};
let proxy = new Proxy(obj, {});
// 访问属性没有差别
console.log(obj.name); // Dasen
console.log(obj.name); // Dasen
// 操作属性没有差别
proxy.age = 21;
console.log(obj.age); // 21
proxy.age--;
console.log(obj.age); // 20
obj.age += 2;
console.log(proxy.age); // 22
// 甚至方法的调用都没有差别
console.log(obj.hasOwnProperty("girlfriend")); // true
console.log(proxy.hasOwnProperty("girlfriend")); // true
console.log(proxy.girlfriend); // null

没有办法用instanceof来判断一个代理对象是不是代理,因为代理的构造函数没有原型,但是可以使用相等判断来区分代理和原对象:

1
2
3
4
5
6
7
8
9
// 相等判断可以区分代理和原对象
console.log(proxy == obj); // false
console.log(proxy === obj); // false
// 代理构造函数没有原型因此不能使用 instanceof 操作符判断对象是不是代理
console.log(typeof obj); // object
console.log(typeof proxy); // object
console.log(obj instanceof Object); // true
console.log(proxy instanceof Object); // true
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check

捕获器

代理的意义在于可以在操作目标对象之前拦截操作并做一些额外的事情,这个拦截就是通过捕获器来进行的。

使用捕获器和反射API

定义捕获器是通过在创建代理时传入的第二个对象来实现的,这个对象的一系列方法描述了一系列捕获器。

比如使用get捕获器拦截从目标对象读取数据的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = {
name: "Dasen",
age: 22,
girlfriend: null
};
let handler = {
get(tar, property, rec) {
if(property==="girlfriend" && tar[property]===null) {
return "无可奉告!";
} else {
return Reflect.get(...arguments);
}
}
}
let proxy = new Proxy(obj, handler);
console.log(proxy.name); // Dasen
console.log(proxy.age); // 22
console.log(obj.girlfriend); // null
console.log(proxy.girlfriend); // 无可奉告!
obj.girlfriend = "someone";
console.log(proxy.girlfriend); // someone

这里定义了一个代理,拦截了目标对象的get操作。JS给对象规定了一系列操作的定义,比如这里的get操作能够拦截到属性访问、方括号访问等一切能访问到属性的操作。get操作能拦截到三个参数,分别是操作的对象、访问的属性和代理对象。

而常规的做法是在一些情况下拦截操作,而另一些情况下放通操作——即返回原来应该得到的结果。这个过程通过反射API即Reflect上的静态方法来进行,如本例中,当大森没有女朋友时,尝试问大森的女朋友是谁,就会回答无可奉告,而大森有女朋友时或者问的是大森的年龄和姓名时,就直接回答应有的结果,就像直接访问了目标对象的属性一样,这时就使用Reflect.get(...arguments)将get捕获器捕获到的参数原样传给这个get静态方法,就能够直接进行原有的默认操作(并将结果返回)。

捕获器不变式

捕获器的定义并非是没有限制的,JS会限制捕获器产生明显过于反常的行为,如目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出TypeError:

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {};
Object.defineProperty(obj,"a",{
value: "aaa",
configurable: false,
writable: false
});
let handler = {
get() {
return "bbb";
}
};
let proxy = new Proxy(obj, handler);
console.log(proxy.a); // TypeError: 'get' on proxy: ...

可撤销代理

代理对象是可以中断与目标对象的联系的,你可能以为需要调用代理对象的什么方法,其实不是的,这个取消关联是通过一个函数实现的,而这个函数是在创建代理时显式地使用Proxy.revocable()静态方法来创建代理时与代理一起返回的,可以通过这种方式来创建一个可撤销的代理:

1
2
3
4
5
6
7
8
9
10
let obj = { a: "aaa" };
let handler = {
get() {
return "hhh";
}
};
let { proxy, revoke } = Proxy.revocable(obj,handler);
console.log(proxy.a); // hhh
revoke(); // 撤销代理
console.log(proxy.a); // TypeError: Cannot perform 'get' on a proxy that has been revoked

也就是说Proxy.revocable()静态方法返回了一个对象,这个对象具有proxy和revoke两个属性,分别代表了新创建的代理和撤销它的函数,我们通过对象解构来取得了这两者。

反射

上文已经使用过了一个反射API,创建了get拦截器,并且在一定情况下使用反射API将操作原样执行或修改了其中的参数后再调用反射API执行。

注意:

  • 反射API其实就是一些对对象的操作的函数形式;
  • 反射API并不局限于在捕获器中使用,在任何可以使用函数的地方都可以使用它来代替对对象的操作。

反射API主要分为三种类别:操作对象的、访问对象的、调用函数的。

操作相关

与操作对象相关的反射API主要有:

  • Reflect.defineProperty()
  • Reflect.preventExtensions()
  • Reflect.setPrototypeOf()
  • Reflect.set()
  • Reflect.deleteProperty()

它们其实都是对对象的某些修改操作封装成的函数,它们都会返回一个布尔值,表示操作成功了没,这在很多时候都是很有用的。比如我们希望为一个对象定义新属性,但是定义可能出现问题,因此我们不得不使用try-catch语句来捕获错误并进行相应处理,而使用反射则可以:

1
2
3
4
5
6
let obj = {};
if(Reflect.defineProperty(obj,"a",{value:"aaa"})) {
console.log("成功了!");
} else {
console.log("失败了……");
}

访问相关

访问相关的反射可以替代对应的访问操作,既然是访问操作,我们当然希望它们返回它们访问到的值,因此它们的返回值就是操作返回的值:

  • Reflect.get():可以替代对象属性访问操作符。
  • Reflect.has():可以替代in操作符或with()。
  • Reflect.deleteProperty():可以替代delete操作符。
  • Reflect.construct():可以替代new操作符。

函数相关

函数调用相关的只有一个Reflect.apply(),用于代替函数的apply()。有时候可能希望通过apply()方法来调用函数,但这个方法位于函数的原型上,万一函数本身定义了自己的apply属性遮蔽了原型上的这个方法(虽然非常不建议使用apply作为属性名),那我们就得这样:

1
Function.prototype.apply.call(myFunc, thisVal, argumentList);

而使用代理,我们就可以这样:

1
Reflect.apply(myFunc, thisVal, argumentsList);

多层代理

多层代理顾名思义就是一个代理代理另一个代理。这样就可以在目标对象之上建立多层拦截网,每层拦截用来做一件特定的事情,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = { name: "" };
let p1 = new Proxy(obj, {
// 拦截 set 操作,将写入的值转换为小写
set(target, property, value, rec) {
if(property !== "name") {
return Reflect.set(...arguments);
}
return Reflect.set(target,property,value.toLowerCase(),rec);
}
});
let p2 = new Proxy(p1, {
// 拦截 set 操作,去掉写入的值的前后空白符
set(target, property, value, rec) {
if(property !== "name") {
return Reflect.set(...arguments);
}
return Reflect.set(target,property,value.trim(),rec);
}
});
p2.name = " Dasen ";
console.log(p2.name); // dasen

代理类

在调用方法时,方法中的this应当指向调用这个方法的对象,假如目标对象有两个方法分别为inner()outer(),其中在outer()方法中又调用了自身的this.inner()方法,那么如果将该对象通过代理操作,这里的this应当为代理对象,也就相当于调用了proxy.inner()proxy.outer()方法。

当然这在大多数情况下是没有问题的,操作都可以正常映射到目标对象,但是在进行一些依赖对象标识的操作中就不一定了,比如使用WeakMap来保存私有变量的时候,以对象标识符作为WeakMap的键就会出现问题,代理对象作为键是读取不到目标对象的私有变量的。

这时的解决方法是不再代理类的对象实例,而是直接代理类,在JS中类实际就是一个构造函数,函数也是一种对象,所以代理完全可以代理类,再通过代理类来创建的对象就可以正常使用那些依赖于实例标识符的操作了。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 代理对象
const wm = new WeakMap();
class User {
constructor(userid) {
wm.set(this, userid);
}
set id(userid) {
wm.set(this, userid);
}
get id() {
return wm.get(this);
}
}
const user = new User(555);
console.log(user.id); // 555
const proxy = new Proxy(user,{});
console.log(proxy.id); // undefined
// 代理类
const ProxyUser = new Proxy(User,{});
const proxyUser = new ProxyUser(666);
console.log(proxyUser.id); // 666

可以看到这种解决方案的思路是这样的:不是要以对象标识符为键吗?那好,既然代理对象和目标对象不是同一个对象,没法访问到同一组私有变量,那就直接代理类本身,这样从代理类创建出的对象就好像是融合了目标对象和代理对象一样,既能进行目标对象的所有操作,代理的捕获器也能正常工作,融合在一起之后就是同一个对象了,也不必担心对象标识符的问题了。

除此之外由于代理的this指向导致的问题还有一些,主要是在代理内置类型的时候产生的,因为很多内置类型的行为是依赖一些对象的内部槽位的,这些内部槽位不能被开发者所访问,代理对象中也不会有这些槽位,因此使用代理时可能会出现问题。

捕获器与反射方法

代理可以捕获13种不同的基本操作,这些操作有各自不同的反射API方法、参数、关联的操作和不变式。

API解释

代理的捕获器和反射的API是一一对应的,如一个xxx()捕获器,必定对应着一个Reflect.xxx()反射,并且它们接收的参数和返回值都完全相同。在一个代理对象上进行的任何一个操作,都只有一个捕获器会被触发,不会发生重复捕获的情况。

捕获器和反射方法可能接受的参数主要有以下:

  • target:目标对象。
  • property:操作的属性名。
  • receiver:代理对象。
  • value:要赋给属性的值。
  • descriptor:属性配置描述,包含可选的enumerable、configurable、writable、value、get和set定义的对象。
  • prototype:要替换target原型的对象,如果是顶级原型则为null。
  • thisArg:调用函数时的this参数。
  • argumentsList:传给目标函数的参数列表。

捕获器参考

get捕获器

  • API:get(target, property, receiver)
  • 返回值:返回取得的属性的值。
  • 拦截的操作:
    • proxy.property
    • proxy[property]
    • Object.create(proxy)[property]
    • Reflect.get(proxy, property, receiver)
  • 捕获器不变式:
    • 如果target.property不可写且不可配置,则捕获器返回的值必须与target.property相同;
    • 如果target.property不可配置且[[Get]]特性为undefined,捕获器返回值必须是undefined。

set捕获器

  • API:set(target, property, value, receiver)
  • 返回值:返回true表示成功;返回false表示失败,严格模式下失败会抛出TypeError。
  • 拦截的操作:
    • proxy.property = value
    • proxy[property] = value
    • Object.create(proxy)[property] = value
    • Reflect.set(proxy, property, value, receiver)
  • 捕获器不变式:
    • 如果target.property不可写且不可配置,则不能修改目标属性的值;
    • 如果target.property不可配置且[[Set]]特性为undefined,则不能修改目标属性的值。

has捕获器

  • API:has(target, property)
  • 返回值:has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
  • 拦截的操作:
    • property in proxy
    • property in Object.create(proxy)
    • with(proxy) { (property); }
    • Reflect.has(proxy, property)
  • 捕获器不变式:
    • 如果target.property存在且不可配置,则处理程序必须返回true;
    • 如果target.property存在且目标对象不可扩展,则处理程序必须返回true。

defineProperty捕获器

  • API:defineProperty(target, property, descriptor)
  • 返回值:defineProperty()必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。
  • 拦截的操作:
    • Object.defineProperty(proxy, property, descriptor)
    • Reflect.defineProperty(proxy, property, descriptor)
  • 捕获器不变式:
    • 如果目标对象不可扩展,则无法定义属性;
    • 如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性;
    • 如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性。

getOwnPropertyDescriptor捕获器

  • API:getOwnPropertyDescriptor(target, property)
  • 返回值:getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回undefined。
  • 拦截的操作:
    • Object.getOwnPropertyDescriptor(proxy, property)
    • Reflect.getOwnPropertyDescriptor(proxy, property)
  • 捕获器不变式:
    • 如果自有的target.property存在且不可配置,则处理程序必须返回一个表示该属性存在的对象;
    • 如果自有的target.property存在且可配置,则处理程序必须返回表示该属性可配置的对象;
    • 如果自有的target.property存在且target不可扩展,则处理程序必须返回一个表示该属性存在的对象;
    • 如果target.property不存在且target不可扩展,则处理程序必须返回undefined表示该属性不存在;
    • 如果target.property不存在,则处理程序不能返回表示该属性可配置的对象。

deleteProperty捕获器

  • API:deleteProperty(target, property)
  • 返回值:必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。
  • 拦截的操作:
    • delete proxy.property
    • delete proxy[property]
    • Reflect.deleteProperty(proxy, property)
  • 捕获器不变式:
    • 如果自有的target.property存在且不可配置,则处理程序不能删除这个属性。

ownKeys捕获器

  • API:ownKeys(target)
  • 返回值:必须返回包含字符串或符号的可枚举对象。
  • 拦截的操作:
    • Object.getOwnPropertyNames(proxy)
    • Object.getOwnPropertySymbols(proxy)
    • Object.keys(proxy)
    • Reflect.ownKeys(proxy)
  • 捕获器不变式:
    • 返回的可枚举对象必须包含target的所有不可配置的自有属性;
    • 如果target不可扩展,则返回可枚举对象必须准确地包含自有属性键。

getPrototypeOf捕获器

  • API:getPrototypeOf(target)
  • 返回值:必须返回对象或null。
  • 拦截的操作:
    • Object.getPrototypeOf(proxy)
    • Reflect.getPrototypeOf(proxy)
    • proxy.__proto__
    • Object.prototype.isPrototypeOf(proxy)
    • proxy instanceof Object
  • 捕获器不变式:
    • 如果target不可扩展,则Object.getPrototypeOf(proxy)唯一有效的返回值就是Object.getPrototypeOf(target)的返回值。

setPrototypeOf捕获器

  • API:setPrototypeOf(target, prototype)
  • 返回值:必须返回布尔值,表示原型赋值是否成功。返回非布尔值会被转型为布尔值。
  • 拦截的操作:
    • Object.setPrototypeOf(proxy)
    • Reflect.setPrototypeOf(proxy)
  • 捕获器不变式:
    • 如果target不可扩展,则唯一有效的prototype参数就是Object.getPrototypeOf(target)的返回值。

isExtensible捕获器

  • API:isExtensible(target)
  • 返回值:必须返回布尔值,表示target是否可扩展。返回非布尔值会被转型为布尔值。
  • 拦截的操作:
    • Object.isExtensible(proxy)
    • Reflect.isExtensible(proxy)
  • 捕获器不变式:
    • 如果target可扩展,则处理程序必须返回true;
    • 如果target不可扩展,则处理程序必须返回false。

preventExtensions捕获器

  • API:preventExtensions(target)
  • 返回值:必须返回布尔值,表示target是否已经不可扩展。返回非布尔值会被转型为布尔值。
  • 拦截的操作:
    • Object.preventExtensions(proxy)
    • Reflect.preventExtensions(proxy)
  • 捕获器不变式:
    • 如果Object.isExtensible(proxy)是false,则处理程序必须返回true。

apply捕获器

  • API:apply(target, thisArg, argumentsList)
  • 返回值:返回值无限制。
  • 拦截的操作:
    • proxy(...argumentsList)
    • Function.prototype.apply(thisArg, argumentsList)
    • Function.prototype.call(thisArg, ...argumentsList)
    • Reflect.apply(target, thisArgument, argumentsList)
  • 捕获器不变式:
    • target必须是一个函数对象。

construct捕获器

  • API:construct(target, argumentsList, receiver)
  • 返回值:必须返回一个对象。
  • 拦截的操作:
    • new proxy(...argumentsList)
    • Reflect.construct(target, argumentsList, receiver)
  • 捕获器不变式:
    • target必须可以用作构造函数。

代理模式

监听属性

通过get、has捕获器可以监听某个对象的属性何时被访问,如:

1
2
3
4
5
6
7
8
9
10
const obj = { name: "Dasen" };
const proxy = new Proxy(obj, {
get(target, p) {
console.log("obj 的属性",p,"被访问了!");
return Reflect.get(...arguments);
}
});
console.log(proxy.name);
// obj 的属性 name 被访问了!
// Dasen

同时还可以使用set捕获器实现两个对象的数据双向绑定,将一个对象的属性更改同步到其他对象,如:

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
const obj1 = { val: "" };
const obj2 = { val: "" };
const proxy1 = new Proxy(obj1, {
set(target, p, v) {
if(p !== "val") {
return Reflect.set(...arguments);
}
obj2[p] = v;
return Reflect.set(...arguments);
}
});
const proxy2 = new Proxy(obj2, {
set(target, p, v) {
if(p !== "val") {
return Reflect.set(...arguments);
}
obj1[p] = v;
return Reflect.set(...arguments);
}
});
proxy1.val = "val1";
console.log(proxy1.val); // val1
console.log(proxy2.val); // val1
proxy2.val = "val2";
console.log(proxy1.val); // val2
console.log(proxy2.val); // val2

隐藏属性

可以在通过get和has捕获器在访问对象的属性时隐藏掉该属性,使得外部无法访问到隐藏属性:

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
const hiddenProperties = ["age", "weight"];
const obj = {
name: "Dasen",
age: 22,
height: 180,
weight: 135
};
const proxy = new Proxy(obj, {
get(t,p) {
if(hiddenProperties.includes(p)) {
return undefined;
}
return Reflect.get(...arguments);
},
has(t,p) {
if(hiddenProperties.includes(p)) {
return false;
}
return Reflect.has(...arguments);
}
});
console.log(proxy.name); // Dasen
console.log(proxy.age); // undefined
console.log(proxy.height); // 180
console.log(proxy.weight); // undefined

对象属性验证

在给对象属性赋值时可以对值的合法性进行验证,从而拒绝某些不合理的赋值操作:

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
const obj = {
name: "Dasen",
age: 22,
height: 180,
weight: 135
};
const proxy = new Proxy(obj, {
set(o,p,v) {
if(p==="weight") {
if(typeof v !== "number" || v <= 0) {
console.log("体重只能是大于0的数字!");
return false;
} else if(v > 180) {
console.log("大森是不可能那么胖的!");
return false;
}
}
return Reflect.set(...arguments);
}
});
proxy.weight = 130;
console.log(proxy.weight); // 130
proxy.weight = 185; // 大森是不可能那么胖的!
console.log(proxy.weight); // 130
proxy.weight = "135"; // 体重只能是大于0的数字!
console.log(proxy.weight); // 130
proxy.weight = -1; // 体重只能是大于0的数字!
console.log(proxy.weight); // 130

函数参数验证

可以通过对函数的代理来验证传入函数的参数是否合法:

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
function sum(arr) {
// 求数组中所有数的和
return arr.reduce((r,c) => r+c);
}
const proxy = new Proxy(sum, {
apply(t,thisArg,arg) {
let arrArg = arg[0];
if(!Array.isArray(arrArg)) {
console.log("要传入一个数组作为参数!");
return undefined;
}
for (const i of arrArg) {
if(typeof i !== "number") {
console.log("数组中的每一个值都应该是数值!");
return undefined;
}
}
return Reflect.apply(...arguments);
}
});
console.log(proxy([1,2,3,4,5])); // 15
console.log(proxy(1));
// 要传入一个数组作为参数!
// undefined
console.log(proxy(["1",2,"3"]));
// 数组中的每一个值都应该是数值!
// undefined

还可以对构造函数的传入值进行约束,要求构造时必须传参等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User {
constructor(userid) {
this.id = userid;
}
}
const proxy = new Proxy(User, {
construct(o,argArr) {
if(argArr[0] === undefined) {
throw "必须要传入UserID!";
}
return Reflect.construct(...arguments);
}
});
new proxy(1);
new proxy(); // Error: 必须要传入UserID!

观察者模式

观察者模式是使用一个中间人(第三方)作为观察者,在对某个类实例化的时候,将类实例化的消息传递给观察者,这样实例对象就纳入了观察者的管理,再对管理者绑定代理,这样管理者就可以对实例对象进行管理了。

如下面的例子就是对于每一个新创建的用户加入观察者的观察中,观察者在有新的用户加入时发布消息:

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
const userList = [];
const watcher = new Proxy(userList, {
set(target, p, value) {
const res = Reflect.set(...arguments);
if(p !== "length" && res) {
console.log("欢迎",value.name,"的加入!");
}
return res;
}
})
class User {
constructor(username) {
this.name = username;
}
}
const userProxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments);
watcher.push(newUser);
return newUser;
}
});
new userProxy("Dasen"); // 欢迎 Dasen 的加入!
new userProxy("Three"); // 欢迎 Three 的加入!
new userProxy("Jack"); // 欢迎 Jack 的加入!



* 你好,我是大森。如果文章内容帮到了你,你可通过下方付款二维码支持作者 *