ES6功能扩展-代理(Proxy)和反射(Reflect)
代理和反射
代理
代理(Proxy)是一种可以拦截并改变底层JS引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象
通过new Proxy()可以创建目标对象的代理,它虚拟化了目标,所以两者看起来功能一致
代理可以拦截JS引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数
反射
反射API以Reflect对象的形式出现,Reflect对象中方法的默认特性与JS引擎的底层操作一致,而代理可以覆盖这些操作,所以每个代理陷阱都可以覆写JS对象的一些内建特性。如果仍需要使用这些内建特性,可以使用相应的反射(Reflect) API 方法。
下表总结了代理陷阱的特性:
代理陷阱 | 覆写的特性 | 默认特性 |
---|---|---|
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.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() | Reflect.defineProperty() |
ownKeys | Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
construct | 用new调用一个函数 | Reflect.construct() |
创建简单代理
用Proxy构造函数创建代理需要传入两个参数:目标对象和处理程序。处理程序用于定义陷阱对象。
let target = {}
let proxy = new Proxy(target, {})
proxy.name = 'hello'
console.log(proxy.name, target.name) // hello hello
target.name = 'world'
console.log(proxy.name, target.name) // world world
示例中的proxy是一个代理,把为其添加的name属性转发给了target,它自身不会存储这个属性。由于proxy.name
和target.name
引用的都是target.name
,因此两者的值相同
set陷阱
任务:创建一个对象,当为对象新增属性时,验证属性值是不是一个数字,如果不是就抛出错误。
现在用set陷阱覆写设置值的默认特性,set陷阱接收4个参数
trapTarget: 用于接收代理目标的对象
key: 要写入的属性键
value: 被写入的属性值
receiver: 操作发生的对象(通常是代理)
let target = {name: 'hello'}
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
console.log(trapTarget, receiver)
// 如果新添加了一个属性,并且属性值不是数字
if(!trapTarget.hasOwnProperty(key) && isNaN(value)) {
throw new TypeError('Property must be a number.')
}
// 添加属性
return Reflect.set(trapTarget, key, value, receiver)
}
})
proxy.name = 'wmui' // 已有属性可以赋值任意值
console.log(proxy.name, target.name) // wmui wmui
proxy.count = 1 // 新增属性可以赋值数字
console.log(proxy.count, target.count) // 1 1
proxy.age = '18岁' // 新增属性赋值非数字报错
示例中trapTarget就是target, receiver就是代理
Reflect.set()是set陷阱对应的默认反射方法,它和set陷阱接收相同的参数
set代理陷阱可以拦截写入属性的操作,get代理陷阱可以拦截读取属性的操作
get陷阱
使用get陷阱验证对象结构
任务:当读取对象上不存在的属性时抛出错误,而不是返回默认值undefined
get陷阱接收3个参数
trapTarget 被读取属性的源对象
key 要读取的属性键(字符串或Symbol)
receiver 操作发生的对象(通常是代理)
let obj = new Proxy({}, {
get(trapTarget, key, receiver) {
// 如果属性不在代理对象上
if(!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.")
}
return Reflect.get(trapTarget, key, receiver)
}
})
obj.name = 'hello'
console.log(obj.name) // hello
console.log(obj.age) // Uncaught TypeError: Property age doesn't exist.
has陷阱
使用has陷阱隐藏已有属性
任务:in操作符来检测给定对象是否含有某个属性,如果拥有这个属性则返回true。现在使用has陷阱改变一部分属性被in检测时的行为,即隐藏已有属性。
has陷阱接收2个参数
trapTarget读取属性的对象(代理的目标)
key要检查的属性键(字符串或Symbol)
let target = {
name: 'wmui',
age: 18
}
console.log('name' in target, 'toString' in target) // true true
默认情况下,对象自有的属性和继承自Object的属性都可以被in操作符检测到
现在使用Reflect.has()方法隐藏name属性
let target = {
name: 'wmui',
age: 18
}
let proxy = new Proxy(target, {
has(trapTarget, key) {
if(key === 'name') {
return false
}
return Reflect.has(trapTarget, key)
}
})
console.log('name' in proxy, 'toString' in proxy) // false true
deleteProperty陷阱
使用deleteProperty陷阱防止删除对象属性
任务:不允许使用delete操作符强制删除某个属性
delete操作符可以从对象中移除属性,如果成功则返回true,不成功则返回false。如果删除一个不可配置属性,在非严格模式下返回false,严格模式下直接报错
let target = {
name: 'wmui',
age: 18
}
target.defineProperty(target, 'name', {
configurable: false
})
let ret = delete target.name
console.log(ret, 'name' in target)
// false true
// 严格模式下报错 Uncaught TypeError: Cannot delete property 'name'
Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,通过delete操作符删除对象属性时,deleteProperty陷阱会被调用,它接收两个参数
trapTarget 要删除属性的对象(代理的目标)
key 要删除的属性键(字符串或Symbol)
let target = {
name: 'wmui',
age: 18
}
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if(key === 'name') {
return false
}
return Reflect.deleteProperty(trapTarget, key)
}
})
let ret = delete proxy.name
console.log(ret, 'name' in proxy) // false true
如果希望保护属性不被删除,而且在严格模式下不抛出错误,那么这个方法非常有用
原型代理陷阱
原型代理陷阱指的是setPrototypeOf()陷阱和getPrototype()陷阱
setPrototypeOf()陷阱接收两个参数
trapTarget 原型设置的对象(代理的目标)
proto 作为原型使用的对象
getPrototype()陷阱接收一个参数
trapTarget 原型获取的对象(代理的目标)
通过代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以拦截Object.setPrototypeOf()和Object.getPrototypeOf()这两个方法的默认执行过程
注意: getPrototypeOf()陷阱必须返回一个对象或null,否则会报错。在setPrototypeOf()陷阱中,如果返回false则表示操作失败,如果返回了任何不是false的值则表示操作成功
let target = {}
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null
},
setPrototypeOf(trapTarget, proto) {
return false
}
})
let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)
console.log(targetProto === Object.prototype, proxyProto === Object.prototype) // true false
console.log(proxyProto) // null
Object.setPrototypeOf(target, {}) // 成功
Object.setPrototypeOf(proxy, {}) // 报错
如果需要使用这两个陷阱的默认行为,则可以使用Reflect上的相应方法
let target = {}
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget)
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto)
}
})
let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)
console.log(targetProto === Object.prototype, proxyProto === Object.prototype) // true true
Object.setPrototypeOf(target, {}) // 成功
Object.setPrototypeOf(proxy, {}) // 成功
Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法疑似Object.getPrototypeOf()方法和Object.setPrototypeOf()方法,尽管两组方法执行相似的操作,但两者间仍有一些不同之处
两组方法的差异
ES6中提供了Object.setPrototypeOf()方法,它接收和Reflect.setPrototypeOf()相同的参数。ES5中提供了Object.getPrototypeOf()方法,它接收和Reflect.getPrototype()相同的参数。尽管接收的参数一样,行为看起来也相似,但它们还是有一些不同之处
Object.getPrototypeOf()和Object.setPrototypeOf()是给开发者使用的高级操作;而Reflect.getPrototypeOf()方法和Reflect.setprototypeOf()方法则是底层操作,其赋予开发者可以访问之前只在内部操作的[[GetPrototypeOf]]和[[SetPrototypeOf]]的权限
Reflect.getPrototypeOf()方法是内部[[GetprototypeOf]]操作的包裹器,Reflect.setPrototypeOf()方法是内部[[SetprototypeOf]]操作的包裹器。Object上相应的方法虽然也调用了[[GetPrototypeOf]]和[[Setprototypeof]],但在此之前会执行一些额外步骤,并通过检查返回值来决定下一步的操作
比如说如果传入的参数不是对象,Reflect.getPrototypeOf()方法会抛出错误,而Object.getPrototypeOf()方法则会在操作执行前先将参数强制转换为一个对象
let ret = Object.getPrototypeOf(1)
console.log(ret === Number.prototype) // true
Reflect.getPrototypeOf(1) // 报错
Object.getPrototypeOf()方法会强制让数字1变为Number对象,所以可以检索它的原型并得到返回值Number.prototype;而由于Reflect.getPrototypeOf()方法不强制转化值的类型,而且1又不是一个对象,故会抛出一个错误
对于Reflect.setPrototypeOf()方法与Object.setPrototypeOf()方法,Reflect.setPrototypeOf()方法返回一个布尔值来表示操作是否成功,成功时返回true,失败则返回false;而Object.setPrototypeOf()方法成功则返回第一个参数作为它的值,一旦失败则会抛出一个错误
let target1 = {}
let result1 = Object.setPrototypeOf(target1, {})
console.log(result1, result1 === target1) // {} true
let target2 = {}
let result2 = Reflect.setPrototypeOf(target2, {})
console.log(result2, result2 === target2) // true false
除了上面提到的这些差异外,在Object和Reflect上还有更多看似重复的方法,但是在所有代理陷阱中一定要使用Reflect上的方法
isExtensible和preventExtensions陷阱
ES5通过Object.preventExtensions()方法和Object.isExtensible()方法修正了对象的可扩展性,ES6可以通过代理中的preventExtensions和isExtensible陷阱拦截这两个方法并调用底层对象操作。
两个陷阱都接收唯一参数trapTarget对象,isExtensible陷阱返回一个布尔值表示对象是否可扩展,preventExtensions也返回一个布尔值,表示操作是否成功
Reflect.preventExtensions()方法和 Reflect.isExtensible()方法实现相应陷阱中默认行为,二者都返回布尔值
let target = {}
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget)
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget)
}
})
console.log(Object.isExtensible(target), Object.isExtensible(proxy)) // true true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(target), Object.isExtensible(proxy)) // false false
这个示例实现了isExtensible和preventExtensions陷阱的默认行为,如果想让Object.preventExtensions()对于proxy失效,那么可以在preventExtensions陷阱中返回false
let target = {}
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget)
},
preventExtensions(trapTarget) {
return false
}
})
console.log(Object.isExtensible(proxy)) // true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(proxy)) // true
Object.isExtensible()和Object.preventExtensions()方法
Object.isExtensible()方法和Reflect.isExtensible()方法相似,当传入非对象值时,Object.isExtensible()返回false,而Reflect.isExtensible()则抛出一个错误
let result1 = Object.isExtensible(3)
console.log(result1) // false
let result2 = Reflect.isExtensible(3)// 抛出错误
对于Object.preventExtensions()方法和Reflect.preventExtensions()方法,无论传入Object.preventExtensions()方法的参数是否为一个对象,它总是返回该参数;而如果Reflect.preventExtensions()方法的参数不是对象就会抛出错误,如果参数是一个对象,操作成功时会返回true,否则返回false
let result1 = Object.preventExtensions(3)
console.log(result1) // 3
let result2 = Reflect.preventExtensions(3) // 抛出错误
属性描述符陷阱
ES5中可以通过Object.defineProperty()方法定义属性特性,并且可以通过Object.getOwnPropertyDescriptor()方法来获取这些属性
在代理中可以分别用defineProperty陷阱和getOwnPropertyDescriptor陷阱拦截 Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法的调用。definePropepty陷阱接受以下参数
trapTarget 要定义属性的对象(代理的目标)
key 属性的键(字符串或Symbol)
descriptor 属性的描述符对象
defineProperty陷阱需要在操作成功后返回true,否则返回false。getOwnPropertyDescriptor陷阱只接受trapTarget和key两个参数,最终返回描述符。
Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法与对应的陷阱接受相同参数。
let target = {}
let proxy = new Proxy(target, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor)
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key)
}
})
Object.defineProperty(proxy, 'name', { value: 'proxy' })
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name')
console.log(proxy.name, descriptor.value) // proxy proxy
这个示例展示了陷阱的默认行为
defineProperty陷阱返回布尔值来表示操作是否成功。返回true时,Object.defineProperty()方法成功执行;返回false时,Object.defineProperty()方法抛出错误。这个功能可以用来限制Object.defineProperty()方法可定义的属性类型,例如,如果希望阻止Symbol类型的属性,则可以当属性键为symbol时返回false
let target = {}
let proxy = new Proxy(target, {
defineProperty(trapTarget, key, descriptor) {
if(typeof key === 'symbol') {
return false
}else{
return Reflect.defineProperty(trapTarget, key, descriptor)
}
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key)
}
})
Object.defineProperty(proxy, 'name', { value: 'proxy' }) // 正常
let nameSymbol = Symbol()
Object.defineProperty(proxy, nameSymbol, { value: 'nameSymbol' }) // 报错
这个示例中,调用Object.defineProperty()并传入'name',因此键的类型是字符串所以方法成功执行;调用Object.defineProperty()方法并传入nameSymbol,defineProperty陷阱返回false所以抛出错误
描述符对象限制
无论将什么对象作为第三个参数传递给Object.defineProperty()方法,都只有属性enumerable、configurable、value、writable、get和set将出现在传递给defineProperty陷阱的描述符对象中
let target = {}
let proxy = new Proxy(target, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value, descriptor.name) // proxy undefined
return Reflect.defineProperty(trapTarget, key, descriptor)
}
})
Object.defineProperty(proxy, 'name', {
value: 'proxy', // 标准
name: 'custom' // 非标准
})
示例中,调用Object.defineProperty()时传入包含非标准name属性的对象作为第三个参数,当defineProperty陷阱被调用时,descriptor对象有value属性却没有name属性,这是因为descriptor不是实际传入Object.defineProperty()方法的第三个参数的引用,而是一个只包含那些被允许使用的属性的新对象。Reflect.defineProperty()方法同样也忽略了描述符上的所有非标准属性
对于getOwnPropertyDescriptor陷阱,它的返回值必须是null、undefined或一个对象。如果返回对象,则对象自己的属性只能是enumepable、configurable、value、writable、get和set,在返回的对象中使用不被允许的属性会抛出一个错误
let target = {}
let proxy = new Proxy(target, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: 'custom'
}
}
})
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name') // 报错
属性描述符中不允许有name属性,所以当调用Object.getOwnPropertyDescriptor()时抛出了错误
两组方法的差异
Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法与Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法做了同样的事情,看起来很相似,但它们也有一些不同
Object.defineProperty()方法和Reflect.defineProperty()方法只有返回值不同:Object.defineProperty()方法返回第一个参数,而Reflect.defineProperty()的返回值与操作有关,成功则返回true,失败则返回false
// 示例1
let target = {}
let result = Object.defineProperty(target, 'name', { value: 'target'})
console.log(result, target === result) // {name: "target"} true
// 示例2
let target = {}
let result = Reflect.defineProperty(target, 'name', { value: 'reflect'})
console.log(result, target === result) // true fasle
Object.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,内部将这个值强制转换为一个对象;而调用Reflect.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,则抛出一个错误
// 示例1
let descriptor1 = Object.getOwnPropertyDescriptor(3, 'name');
console.log(descriptor1) // undefined
// 示例2
let descriptor2 = Reflect.getOwnPropertyDescriptor(3, 'name') // 抛出错误
ownKeys陷阱
ownKeys代理陷阱可以拦截内部方法[[OwnPropertyKeys]],通过返回数组的值可以覆盖其行为。这个数组被用于Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()4个方法
ownKeys陷阱通过Reflect.ownKeys()方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和Symbol类型的都包含在内。Object.getOwnPropertyNames()方法和Object.keys()方法返回的结果将Symbol类型的属性名排除在外,Object.getOwnPropertySymbols()方法返回的结果将字符串类型的属性名排除在外。Object.assign()方法支持字符串和Symbol两种类型
ownKeys陷阱唯一接受的参数是操作的目标,返回值必须是一个数组或类数组对象,否则就抛出错误。当调用Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()或Object.assign()方法时,可以用ownKeys陷阱来过滤掉不想使用的属性键。
假设不想引入任何以下划线字符(在JS中下划线符号表示字段是私有的)开头的属性名称,则可以用ownKeys陷阱过滤掉那些键
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key === 'symbol' || key[0] !== '_'
})
}
})
let nameSymbol = Symbol()
proxy.name = 'proxy'
proxy._name = 'private'
proxy[nameSymbol] = 'symbolName'
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy)
console.log(names, keys, symbols) // ["name"] ["name"] [Symbol()]
for(const key in proxy) {
console.log(key) // name
}
这个示例只返回Symbol类型以及非_
开头的属性。ownKeys代理陷阱也会影响for/in循环。
apply和construct陷阱
所有的代理陷阱中,只有apply和construct的代理目标是一个函数。函数有两个内部方法[[Call]]和[[Construct]],apply陷阱和construct陷阱可以覆盖这些内部陷阱,若使用new操作符调用函数,则执行construct陷阱;若不用new调用,则调用apply陷阱
apply陷阱和Reflect.apply()接受以下参数
trapTaqget 被执行的函数(代理的目标)
thisArg 函数被调用时内部this的值
argumentsList 传递给函数的参数数组
construct陷阱和Reflect.construct接收以下参数
trapTarget 被执行的函数(代理的目标)
argumentsList 传递给函数的参数数组
Reflect.construct()方法除了接收这两个参数,还可以接收可选的第三个参数newTarget,该参数用于指定函数内部new.target的值
使用apply和construct陷阱,可以完全控制任何代理目标函数的行为
let target = function() { return 3 }
let proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList)
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList)
}
})
// 使用了函数的代理,其目标对象会被视为函数
console.log(typeof proxy, proxy()); // function 3
let instance = new proxy()
console.log(instance instanceof proxy, instance instanceof target) // true true
示例中使用了apply陷阱和construct陷阱,代理函数与目标函数的结果完全相同。当用new调用时创建一个instance对象,它同时是代理和目标的实例,因为instanceof通过原型链来确定此信息,而原型链查找不受代理影响,这也就是代理和目标好像有相同原型的原因
应用:验证函数参数
现在有一个求和函数,要求所有参数都属于Number类型,如果不符合要求就抛出错误
// 求和函数
function sum(...values) {
return values.reduce((prev, current) => prev + current, 0)
}
// 代理
let sumProxy = new Proxy(sum, {
apply(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== 'number') {
throw new TypeError('All arguments must be numbers.')
}
})
return Reflect.apply(trapTarget, thisArg, argumentList)
},
// 不允许使用new调用
construct: function(trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.")
}
})
console.log(sumProxy(1, 2, 3, 4)) // 10
console.log(sumProxy(1, "2", 3, 4)) // 报错
let ret = new sumProxy() // 报错
应用:可调用的类构造函数
类调用时必须使用new操作符,因为类构造函数的内部方法[[Call]]被指定来抛出一个错误。由于类构造函数是也是函数,所以可以使用代理拦截对[[Call]]方法的调用。如果希望类构造函数不用new就可以运行,那么可以使用apply陷阱来创建一个新实例
class Person {
constructor(name) {
this.name = name
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList)
}
})
let me = PersonProxy('wmui')
console.log(me.name) // wmui
console.log(me instanceof Person) // true
console.log(me instanceof PersonProxy) // true
可撤销代理
通常在创建代理后,代理不能脱离其目标,如果脱离了目标,代理便失去效力。如果希望创建的代理可以脱离目标,可以使用proxy.revocable()方法创建可撤销的代理,该方法采用与Proxy构造函数相同的参数:目标对象和代理处理程序,返回值是具有以下属性的对象
proxy 可被撤销的代理对象
revoke 撤销代理要调用的函数
let target = {
name: 'target'
}
let {proxy, revoke} = Proxy.revocable(target, {})
console.log(proxy.name) // target
revoke()
console.log(proxy.name) // 报错
示例创建一个可撤销代理,它使用解构功能将proxy和revoke变量赋值给Proxy.revocable()方法返回的对象上的同名属性。之后proxy对象可以像不可撤销代理对象一样使用,一旦revoke()函数被调用,代理不再是函数,尝试访问proxy.name会抛出一个错误