前面的话
代理服务器和反射服务器的解释定义
ES5和ES6致力于为开发者提供JS已有却不可调用的功能。例如在ES5出现以前,JS环境中的对象包含许多不可枚举和不可写的属性,但开发者不能定义自己的不可枚举或不可写属性,于是ES5引入了 Object.defineProperty() 方法来支持开发者去做JS引擎早就可以实现的事情。ES6添加了一些内建对象,赋予开发者更多访问JS引擎的能力。代理( Proxy )是一种可以拦截并改变底层JS引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。本文将详细介绍代理( Proxy )和反射( Reflection )。
引入
在ES6之前,开发者不能通过自己定义的对象模仿JS数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的 length 属性,也可以通过 length 属性修改数组元素:
let colors = ["red", "green", "blue"];
console.log(colors.length); // => 3
colors[3] = "black";
console.log(colors.length); // => 4
console.log(colors[3]); // => "black"
colors.length = 2;
console.log(colors.length); // => 2
console.log(colors[3]); // => undefined
console.log(colors[2]); // => undefined
console.log(colors[1]); // => "green"
colors 数组一开始有 3 个元素,将 colors[3] 赋值为 "black" 时, length 属性会自动增加到 4 ,将 length 属性设置为 2 时,会移除数组的后两个元素而只保留前两个。在ES5之前开发者无法自己实现这些行为,现在通过代理可以实现。
代理和反射
调用 new Proxy() 可创建代替其他目标( target )对象的代理,它虚拟化了目标,所以二者看起来功能一致。
代理可以拦截JS引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
反射API以 Reflect 对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。下表总结了代理陷阱的特性。
代理陷阱 覆写的特性 默认特性
get 读取一个属性值 Reflect.get()
set 写入一个属性 Reflect.set()
has in 操作符 Reflect.has()
deleteProperty delete 操作符 Reflect.delete()
getProperty Object.getPropertypeOf() Reflect.getPrototypeOf()
setProperty Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys() 、
Object.getOwnPropertyNames()、
Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 调用一个函数 Reflect.apply()
construct 用 new 调用一个函数 Reflect.construct()
每个陷阱覆写JS对象的一些内建特性,可以用它们拦截并修改这些特性。如果仍需使用内建特性,则可以使用相应的反射API方法。
创建简单代理
用 Proxy 构造函数创建代理需要传入两个参数:目标( target )和处理程序( handler )。处理程序用于定义一个或多个陷阱的对象,在代理中,除了专门为操作定义的陷阱外,其余操作均使用默认特性。不使用任何陷阱的处理程序等价于简单的转发代理。
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // => "proxy"
console.log(target.name); // => "proxy"
target.name = "target";
console.log(proxy.name); // => "target"
console.log(target.name); // => "target"
这个示例中的代理将所有操作直接转发到目标,将 "proxy" 赋值给 proxy.name 属性时会在目标上创建 name ,代理只是简单地将操作转发给目标,它不会储存这个属性。由于 proxy.name和 target.name 引用的都是 target.name ,因此二者的值相同,从而为 target.name 设置新值后, proxy.name 也一同变化。
陷阱代理
使用 set 陷阱验证属性
假设创建一个属性值是数字的对象,对象中每新增一个属性都要加以验证,如果不是数字必须抛出错误。为了实现这个任务,可以定义一个 set 陷阱来覆写设置值的默认特性。
set 陷阱接受 4 个参数:
trapTaqget 用于接收属性(代理的目标)的对象
key 要写入的属性键(字符串或 Symbol 类型)
value 被写入属性的值
receiver 操作发生的对象(通常是代理)
Reflect.set() 是 set 陷阱对应的反射方法和默认特性,它和 set 代理陷阱一样也接受相同的 4 个参数,以方便在陷阱中使用。如果属性已设置陷阱应该返回 true ,如果未设置则返回 false 。( Reflect.set() 方法基于操作是否成功来返回恰当的值)。
可以使用 set 陷阱并检查传入的值来验证属性值:
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 忽略已有属性,避免影响它们
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // => 1
console.log(target.count); // => 1
// 你可以为 name 赋一个非数值类型的值,因为该属性已经存在
proxy.name = "proxy";
console.log(proxy.name); // => "proxy"
console.log(target.name); // => "proxy"
// 抛出错误
proxy.anotherName = "proxy";
这段代码定义了一个代理来验证添加到 target 的新属性,当执行 proxy.count=1 时, set 陷阱被调用,此时 trapTarget 的值等于 target , key 等于 "count" , value 等于 1 , receiver 等于 proxy 。
由于 target 上没有 count 属性,因此代理继续将 value 值传入 isNaN() ,如果结果是 NaN,则证明传入的属性值不是数字,同时也抛出一个错误。在这段代码中, count 被设置为 1 ,所以代理调用 Reflect.set() 方法并传入陷阱接受的 4 个参数来添加新属性。
proxy.name 可以成功被赋值为一个字符串,这是因为 target 已经拥有一个 name 属性,但通过调用 trapTarget.hasownproperty() 方法验证检查后被排除了,所以目标已有的非数字属性仍然可以被操作。
然而,将 proxy.anotherName 赋值为一个字符串时会抛出错误。目标上没有 anotherName 属性,所以它的值需要被验证,而由于 "Proxy" 不是一个数字值,因此抛出错误。
set 代理陷阱可以拦截写入属性的操作, get 代理陷阱可以拦截读取属性的操作。
用get陷阱验证对象结构(Object Shape)
JS有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用 undefined 代替被读取属性的值。
let target = {};
console.log(target.name); // => undefined
在大多数其他语言中,如果 target 没有 name 属性,尝试读取 target.name 会抛出一个错误。但JS却用 undefined 来代替 target.name 属性的值。这个特性会导致重大问题,特别是当错误输入属性名称的时候,而代理可以通过检查对象结构来回避这个问题。
对象结构是指对象中所有可用属性和方法的集合,JS引擎通过对象结构来优化代码,通常会创建类来表示对象,如果可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误。代理让对象结构检验变得简单。
因为只有当读取属性时才会检验属性,所以无论对象中是否存在某个属性,都可以通过 get 陷阱来检测,它接受 3 个参数:
trapTarget 被读取属性的源对象(代理的目标)
key 要读取的属性键(字符串或 Symbol )
receiver 操作发生的对象(通常是代理)
由于 get 陷阱不写入值,所以它复刻了 set 陷阱中除 value 外的其他 3 个参数, Reflect.get() 也接受同样 3 个参数并返回属性的默认值。
如果属性在目标上不存在,则使用 get 陷阱和 Reflect.get() 时会抛出错误:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// 添加属性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // => "proxy"
// 读取不存在属性会抛出错误
console.log(proxy.nme); // => 抛出错误