ES6 Proxy和Reflect
Proxy
一、概述
Proxy意思是代理器,ES6新增特性。Proxy对象用于创建一个对象的代理,这个代理会进行拦截操作,在对目标对象的访问和修改前都需要通过这层拦截,在拦截中进行需要的操作。
二、语法
let proxy = new Proxy(target, handler)
- target:要进行代理的目标对象,可以是任何东西,包括函数。
- handler:代理配置,或者说拦截操作,是带有钩子的对象。其中钩子包括获取或设置target属性的get和set
一个示例:
let target = {}
let proxy = new Proxy(target, {})
proxy.test = 5
console.log(target.test) // 5
console.log(proxy.test) // 5
上述示例创建了一个代理,但是没有任何钩子。此时所有对proxy的操作都直接转发给target,例如设置对象属性和读取属性值。此时,没有设置拦截操作的proxy就是对target的一个透明包装。proxy对象本身没有自己的属性。想要让proxy有意义,就要进行拦截。
Proxy对目标对象支持的拦截操作共13种,包括以下:
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo和proxy['foo']。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy),返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy),返回一个对象。 - isExtensible(target):拦截
Object.isExtensible(proxy),返回一个布尔值。 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)。
其中最常见的拦截是get和set钩子,即在读写时进行。
1、get
get(target, property, receiver)用于拦截读取操作。
- target:目标对象,该对象作为第一个参数传递给new Proxy
- property:目标属性名
- receiver:如果目标属性是一个getter访问器属性,则receiver就是本次读取属性所在的this对象。通常它就是proxy对象本身。
一个示例:
let person = {
name: 'Leon'
}
let proxy = new Proxy(person, {
get: (target, propKey) => {
console.log(`访问对象的${propKey}属性`)
if(propKey in target) return target[propKey]
else throw new ReferenceError('该属性不存在')
}
})
console.log(proxy.name)
console.log(proxy.age)
上述示例对person对象的访问操作进行了拦截,每次访问的时候都会打印出访问信息,最重要的是访问属性存在时会返回访问属性,不存在则抛出异常。
let arr = [1, 2, 3, 4, 5, 6]
let proxy = new Proxy(arr, {
get: (target, propKey, receiver) => {
let ind = Number(propKey)
if(ind < 0) propKey = String(target.length + ind)
return Reflect.get(target, propKey, receiver)
}
})
console.log(proxy[-2])
上述代码让一个数组可以通过负数索引访问元素。
get方法可以继承。
2、set
set(target, property, value, receiver)用于拦截写入操作。
相比get钩子,多出来一个value参数,即目标属性要设置的值。如果写入成功,set应该返回true,否则返回false
let validator = {
set: (obj, prop, value) => {
if(prop === 'age') {
if(!Number.isInteger(value)) throw new TypeError('Age must be a integer')
if(value > 200) throw new RangeError('Nobody can live more than 200 years')
}
obj[prop] = value
return true
}
}
let person = new Proxy({}, validator)
person.age = 24
person.age = 'young'
person.age = 201
上述代码用proxy实现了一个验证:当给person对象设置年龄时只能设置为小于等于200的整数。
3、ownKeys和getOwnPropertyDescriptor
Object.keys,for...in循环和大多数遍历对象属性的方法(包括Object.values)都由ownKeys拦截。此钩子返回一个数组。
let user = {
name: 'John',
age: 20,
_password: '123456'
}
user = new Proxy(user, {
ownKeys(target){
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
for(let key in user) console.log(key)
console.log(Object.keys(user))
console.log(Object.values(user))
上述代码中的代理让对象在属性遍历时隐藏了以_开头的属性。
注意:如果返回对象中并不存在的键,Object.keys并不会列出该键。
let user = {}
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c']
}
})
console.log(Object.keys(user)) // []
for(key in user) console.log(key) // undefined
因为Object.keys仅返回带有enumerable标记的属性。为了检查它,该方法会对每个属性调用[[GetOwnProperty]]来获得属性描述符。在这里,由于没有属性,其描述符为空,没有enumerable标记,因此会被略过。
为了让Object.keys返回一个属性,可以将该属性和enumerable标记存入对象,或者拦截对它的调用[[GetOwnProperty]],使用钩子getOwnPropertyDescriptor并返回描述符enumerable: true
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) {
return {
enumerable: true,
configurable: true
};
}
});
console.log(Object.keys(user))
4、实现私有属性
JS没有private关键字,但私有属性和方法对某些场景是必要的。一个普遍的约定是以下划线_开头的属性和方法是内部的,不该从对象外部访问它们。可以使用Proxy实现这一需求。
私有属性的实现需要这些钩子:
- get,读取私有属性时抛出错误
- set,写入私有属性时抛出错误
- deleteProperty,删除私有属性时抛出错误
- ownKeys,遍历属性时排出私有属性
let user = {
name: 'John',
age: 18,
_password: 'lock'
}
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value;
},
set(target, prop, val) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
console.log(user._password)
user._password = 'test'
delete user._password
console.log(Object.keys(user))
示例中,访问对象中方法时需要调用value.bind(target),将原始对象作为this,这样在调用方法中有关操作私有属性时不会触发拦截钩子。
这种方式存在很多问题,在任何地方都不该使用这种代理。对于类来说,新的ES标准原生支持私有属性,以#为前缀。
5、has
has(target, property)会在使用in运算符时调用。
let range = {
start: 1,
end: 10
}
range = new Proxy(range, {
has(target, prop){
return prop >= target.start && prop <= target.end
}
})
console.log(5 in range)
console.log(20 in range)
6、apply
apply(target, thisArg, args)拦截函数的调用,call和apply操作。
- target:目标对象
- thisArg:目标this值
- args:参数列表
let target = function(){
return 'This is a target function'
}
target = new Proxy(target, {
apply() { return 'This is a proxy' }
})
target()
7、可取消的Proxy
let {proxy, revoke} = Proxy.revocable(target, handler)
此调用返回一个带proxy和revoke函数的对象以将其禁用。
let object = {
data: 'Valuable data'
}
let {proxy, revoke} = Proxy.revocable(object, {})
console.log(proxy.data) // Valuable data
revoke()
console.log(proxy.data) // Error
Reflect
一、概述
Reflect是一个内置对象,提供拦截JS操作的方法,可简化的创建Proxy。与多数全局对象不同,Reflect不是一个构造函数,不能通过new运算符调用,不能把它当作函数调用。Reflect所有属性和方法都是静态的,就像Math对象。
二、Reflect作用
- 存放Object对象的一些语言内部的方法(如Object.defineProperty)。现在某些内部方法同时在Object和Reflect对象上存在,但未来的新方法只会放到Reflect对象上。
- 修改Object方法的返回结果,如Object.defineProperty(obj, name, desc)在无法定义属性时会抛出错误,而Reflect.defineProperty(obj, name, desc)会返回false。
- 把Object操作都变成函数行为。某些Object操作是命令式,如name in obj和delete obj[name],可以用Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)进行同种操作,变为函数行为。
- 简化Proxy。只要是Proxy对象的方法就能在Reflect对象上找到对应方法。Proxy对象就可以方便调用对应的Reflect方法,完成默认行为,作为修改行为的基础。
let user = { name: 'John' }
user = new Proxy(user, {
get(target, prop, receiver){
console.log(`Get ${prop}`)
return Reflect.get(target, prop, receiver)
},
set(target, prop, val, receiver){
console.log(`Set ${prop}=${val}`)
return Reflect.set(target, prop, val, receiver)
}
})
let name = user.name
user.name = 'Tom'
上述示例简单来说,如果Proxy向把调用转发给目标对象,只需要使用相同参数调用Reflect的同名方法。多数情况下可以不用Reflect完成相同的事情。但Reflect的确可以解决一些问题
let user = {
_name: 'Guest',
get name() {
return this._name
}
}
let userProxy = new Proxy(user, {
get(target, prop, receiver){
return target[prop]
}
})
let admin = {
__proto__: userProxy,
_name: 'Admin'
}
console.log(admin.name) // Guest
上述代码出现了预期外的结果,因为读取admin.name时admin对象会从原型链上找,从userProxy中触发get拦截,此时target是上下文的this,即原始的user对象。可以使用Reflect解决这个问题
let user = {
_name: 'Guest',
get name() {
return this._name
}
}
let userProxy = new Proxy(user, {
get(target, prop, receiver){
return Reflect.get(target, prop, receiver) // return Reflect.get(...arguments)
}
})
let admin = {
__proto__: userProxy,
_name: 'Admin'
}
console.log(admin.name) // Admin
Proxy局限
内部插槽(Internal slots)
许多内置对象如Map,Set,Date,Promise都使用了内部插槽。它们类似于属性,但只能在内部使用,仅用于规范。例如,Map将项目存储在[[MapData]]中,内置方法直接访问它们而不通过[[Get]] [[Set]]等内部方法。因此Proxy不能进行拦截。
let map = new Map()
let proxy = new Proxy(map, {})
proxy.set('test', 1) // Error
但有一种解决方法
let map = new Map()
let proxy = new Proxy(map, {
get(target, prop, receiver){
let value = Reflect.get(...arguments)
return typeof value == 'function' ? value.bind(target) : value
}
})
proxy.set('test', 1)
console.log(proxy.get('test'))
类的私有字段也可能发生类似情况。

浙公网安备 33010602011771号