什么是proxy
proxy翻译过来就是代理的意思,那么在javascript中,proxy(代理)是什么意思呢?proxy时ES6提供的新的API,可以用来定义对象的各种基本操作。proxy是一种可以拦截并改变底层javascript引擎操作的包装器,在新语言中通过它暴露内部运作的对象。
数组的问题
在ES6出现以前,开发者不能通过自己定义的对象模仿javascript数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的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之前开发者无法实现这些行为,但是现在通过代理就可以了
注意:数值属性和length属性具有这种非标准行为,因而在es6中数组被认为是奇异对象
代理和反射
调用new proxy()可创建代替其他目标(target)对象的代理,它虚拟花了目标,所以二者看起来功能一致
代理可以拦截javascript引擎内部目标的底层兑现该操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数
反射API以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以复写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法
| 代理陷阱 | 复写特性 | 默认特性 |
| get | 读取一个属性值 | Reflect.get() |
| set | 写入一个属性 | Reflect.set() |
| has | in操作符 | Reflect.has() |
| deleteProperty | delete操作符 | Reflect.deleteProperty() |
| getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
| setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
| isExtensible | Object.IsExtensible() | Reflect.IsExtensible() |
| preventExtensions| Object.preventExtensions() | Reflect.preventExtensions() |
| getOwnPropertyDescriptor | Object.getOwnPertyDescriptor() | Reflect.getOwnPertyDescriptor() |
| defineProperty | Object.defineProperty() | Reflect.defineProperty() |
| OwnKeys | Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbol() | Reflect.OwnKeys() |
| apply | 调用一个函数 | Reflect.apply() |
| construct | 用new调用一个函数 | Reflect.construct |
每个陷阱覆写javascript对象的一些特性,可以用他们拦截并修改这些特性。如果仍需要使用内建特性,则可以使用相应的反射API方法。创建代理会让代理和反射API的关系变得清楚,所以我们最好深入进去看一些示例
创建一个简单的代理
用Proxy构造函数创建代理需要传入两个参数:目标(target)和处理程序(handler)。处理程序时定义一个或多个陷阱的对象,在代理中,除了专门为操作定义的陷阱外,其余操作均使用默认特性。不适用任何陷阱的处理程序等价于简单的转发代理,就像这样
let target = {}
let proxy = new Proxy(target,{});
proxy.name = 'proxy'
console.log(proxy,proxy.name)
console.log(target,target.name)
console.log(proxy == target)
target.name = 'target'
console.log(proxy,proxy.name)
console.log(target,target.name)
得到的结果如图所示
这个示例中的代理将所有操作直接转发到目标,将"proxy"赋值给proxy.name属性时,会在目标上创建name,代理只是简单地将操作转发给目标,它不会储存这个属性。由于proxy.name和target.name引用的都是target.name,因此二者的值相同,从而target.name设置新值后,proxy.name也一同变化
使用set陷阱验证属性
假设你想创建一个属性值是数字的对象,对象中每新增一个属性都要加以验证,如果不是数字必须抛出错误。为了实现这个任务,可以定义一个set陷阱来覆写设置值的默认特性。set陷阱接受4个参数
1.trapTarget 用于接收属性(代理的目标)的对象
2. key被写入的属性键(字符串或Symbol类型)
3. value被写入属性的值
4. 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)) {
console.log(value, isNaN(value))
if (isNaN(value)) {
throw new TypeError("属性必须是数字")
}
}
//添加属性
return Reflect.set(trapTarget, key, value, receiver)
}
})
// 添加一个新属性
proxy.count = 1
console.log(proxy.count)
console.log(target.count)
//由于目标已有name属性因而可以为他赋值
proxy.name = 'proxy'
console.log(proxy.name)
console.log(target.name)
//给不存在的属性赋值会抛出错误
proxy.anotherName = 'proxy'
结果如下图所示
这段代码定义了一个代理来验证添加到target的新属性,当执行prox.count = 1时,set陷阱被调用,此时trapTarget的值等于target,key为"count",value的值等于1,reciever(本例中未使用)等于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)
javascript有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取属性的值,就像在这个示例中
let target = {}
console.log(target.name)//undefined
在大多数语言中,如果target没有name属性,尝试读取target.name会抛出一个错误,但是javascript却用undefined来代替target.name属性的值。如果你曾接触过大型代码库,应该知道这个特性会导致重大问题,特别是当错误输入属性名称的时候,而代理可以通过检查对象的结构来帮助你回避这个问题。
对象结构是指对象中所有可用的属性和方法的集合,javascript引擎通过对象结构来优化代码,通常会创建类来表示对象,如果你可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误,这对我们很有帮助。代理让对象结构变得简单
因为只有当读取属性时,才回检测属性,所以无论对象中是否存在某个属性,都可以通过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("属性" + key + "不存在")
}
return Reflect.get(trapTarget, key, receiver)
}
})
//新添一个属性,程序仍然正常运行
proxy.name = 'proxy'
console.log(proxy.name) // proxy
//如果属性不存在,则抛出错误
console.log(proxy.nme) //抛出错误
此示例中的get陷阱可以拦截属性的读取操作,并通过in操作符来判断receiver上是否具有被读取的属性,这里之所以用in操作符检查receiver而不检查trapTarget,是为了放置receiver代理含有has陷阱。在这种情况下检查trapTarget可能会忽略掉has陷阱,从而得到错误的结果。属性如果不存在会抛出一个错误,否则就是用默认行为。
这段代码展示了如何在没有错误的情况下给proxy添加新属性name,并写入值和读取值。最后一行包含一个输入错误:proxy.nme有可能是proxy.name,由于nme时一个不存在的属性,因为抛出错误