Proxy
一个 Proxy
对象可以包装另一个对象,并且拦截诸如读取,写入等操作。
语法
let proxy = new Proxy(target, handler)
// target 是要包装的对象,可以是任何东西,包括函数
// hander是代理的配置,是一个对象,在这里配置对目标对象的读写等操作的拦截
let target = {}
let proxy = new Proxy(target, {}) // 代理对象proxy,但是是个空代理,透明转发对原始对象的操作
proxy.test = 5;
target.test // 5
proxy.test // 5 代理对象中也可以读取
for(let k in proxy) {console.log(k)} // test 迭代也正常工作
拦截内部方法
JS对于内部对象的操作,在JS规范中都有对应的内部方法,描述了最底层的工作方式。
内部方法和捕捉器必须满足条件,比如[[Set]]写入值成功,需要返回true,否则false
带有get捕捉器的默认值
get(target, property, receiver)
- target,目标对象本身,也就是传递给new Proxy的
- property,目标属性名
- receiver,目标属性本身是个getter访问器属性,则该参数指向读取属性时所在的this对象,有点绕,后面例子看
例子:用get来实现一个对象的默认值,创建一个数组,对不存在的数组项返回0。
let number = [0,1,2]
number = new Proxy(number, {
get(target, prop, receiver) {
if(prop in target) {
return target[prop]
}
return 0 // 返回默认值
}
})
注意:代理对象应覆盖掉原对象
对象一旦被代理,就不希望外部还能访问到换对象,因此用 obj = new Proxy(obj, {...})
的方式覆盖原对象。
set捕捉器
set(target, prop, value, receiver)
前2个参数同 get
- value,需要设置的新值
- receiver,与get捕捉器一样,仅对
setter
访问器属性相关。
例子:我们来设计一个数组,仅支持写入 number
let number = []
number = new Proxy(number, {
set(target, prop, val, receiver) {
if(typeof val === 'number') {
target[prop] = val
return true
}
return false
}
})
ownKeys捕捉器和getOwnPropertyDescriptor捕捉器
这2个捕捉去对 Object.keys/values/entries
for in
循环和大多数其他遍历对象的属性和方法都有用
这里回顾下,for in会循环原型链上带enumerable的非Symbol键
例子:过滤掉_开头的私有属性
let user = {
name: 'Jack',
age: 30,
_password: '123'
}
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
for(let k in user) {console.log(k)} // name, age
Object.values(user) // [Jack, 30]
Object.keys/values
等不会列出对象中不存在的键和值
let user = {}
user = new Proxy(user, {
ownKeys(target) { // 这里期望调用Object.keys时,调用该拦截器,返回了['a', 'b']
return ['a', 'b']
}
})
// 但实际上不会如期望这样返回
Object.keys(user); // []
// Object.keys 会对每个属性调用内部方法 getOwnProperty 来获取属性描述符,如果没有enumerable,则忽略掉
所以要想让上面的代码返回 ['a', 'b']
,要么赋值user提供a,b属性,要么使用拦截器,返回 enumerable: true
let user = {}
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b']
},
// 不管三七二十一,可遍历配置上true
getOwnPropertyDescriptor(target, prop) {
return {
enumerable: true,
configurable: true
}
}
})
Object.keys(user) // [a, b]
综合例子
_开头的属性,一般都是内部属性,不应该从对象外部访问它们。
借助代理对象,我们可以实现拦截对私有属性的访问等一系列操作。
- get实现读取属性时抛错
- set写入私有属性抛错
- deleteProperty 删除属性时抛错
- ownKeys 在使用遍历for in 或 Object.keys类似方法时,排除_开头
```javascript
let user = {
name: ‘john’,
checkPassword() {
}, _password: ‘*’ }this._password; // user对象内部方法,应该访问到_password.
user = new Proxy(user, {
// get方法特别注意
get(target, prop, receiver) {
if(prop.startsWith(‘‘) {
throw new Error(‘访问禁止’)
}
let value = target[prop]
return typeof value === ‘function’ ? value.bind(target) : value
},
set() {
// …不多讲,注意返回true | false
},
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(‘‘));
}
})
<a name="Wz9xz"></a>
### 不好的get实现
应对 对象调用 比如 user.checkPassword()调用,代理成功后,user变成了Proxy后的对象,此时访问的,this指向的是代理对象user,user(proxy)._password,触发get拦截器,被拦截了。所以需要checkPassword方法bind原始对象user,这样就可以访问_password,而不是通过代理对象,不会触发拦截。
但该方法有很大问题,因为方法可能会将未被代理的对象再传递到其他地方,使用起来就会混乱,到底用的是代理对象,还是非代理对象。<br />比如这个 `checkPassword` 方法,可能会将原始对象传递出去
```javascript
let user = {
checkPassword() {
someFn(this)
}
}
user = new Proxy(user, {
get(target, prop) {
let value = target[value]
// 这里target是原user对象
return typeof value === 'function' ? value.bind(target) : value
}
})
user.checkPassword // this指向的是原始target对象,所以里面someFn(this),把原始未经过包装的user传递出去了
has捕捉器,漂亮的语法糖
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
包装函数+apply转发 proxy+apply转发
// 一个常见的函数
function delay(f, time) {
return function(...args){
setTimeout(() => {
f.apply(this, args)
}, time)
}
}
function sayHi(name) {
console.log(name)
}
// 未被包装前,还能访问函数名等
sayHi.name // sayHi
sayHi.length // 1
sayHi = delay(sayHi, 2000)
sayHi.name // ''
sayHi.length // 0
sayHi('jack') // 2s后打印jack
代码的业务需要达到了,但是包装后的sayHi,丢失了其原始函数的属性访问,如函数name,参数length。而 Proxy
则要强大的多。
apply/call
转发操作,在内部触发 [[Call]]
操作,可以通过 apply
拦截器来拦截,因此:
function delay(f, t) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => {
target.apply(thisArg, args)
}, t)
}
})
}
function sayHi(name) {
console.log(name)
}
sayHi = delay(sayHi, 2000)
sayHi('jack') // 2s后打印
sayHi.name // sayHi
sayHi.length // 1
Reflect
一个内建对象,对他的理解其实就是替代 Object
来调用,但实际上,这个配合 Proxy
很方便。
Reflect
的api和Proxy
的拦截器,基本上是一一对应的,包括参数。Reflect
可以我们将操作符(new delete
),当做函数来用。对应的Reflect.construct
,Reflect.deleteProperty
操作转发给原始对象
```javascript let user = { name: ‘John’ }
user = new Proxy(user, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver) }, set(target, prop, value, receiver) { return Reflect.set(target, prop, value, receiver) } })
<a name="h3YN3"></a>
## 差别,Reflect更好
大多数情况,我们可以使用 `target[prop]` 替代 `Reflect.get(target, prop, receiver)` 。但是他们也有细微差别。使用 `Reflect.get` 更好。
```javascript
let user = {
_name: 'Guest',
get name() {return this._name} // 这是一个getter访问器属性
}
user = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]
}
})
user.name // Guest 没啥问题
但是如果你使用了继承,target的指向就有问题
let admin = Object.create(user)
admin._name = 'Admin'
admin.name // Guest !!! 不是Admin吗?
// 因为getter访问器里,this指向的是target(原始user)
因为这种情况,导致访问失败,而Reflect可以解决
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
receiver保留了正确的this引用,当 admin.name
时,this就是 admin
。
更简单的写法
let userProxy = new Proxy(user, {
get(...args) { // receiver = admin
return Reflect.get(...args); // (*)
}
});
Proxy的局限性
无法正常代理某些使用内部插槽的内建对象
一些内建对象,如 Map
等,使用了所谓的内部插槽(内部属性[[xx]]),内部有些实现,并不是通过Proxy的拦截器对应的内部方法来实现的。
比如Map,Map通过this[[MapData]],在内建方法中可以直接访问它们,而不是 [[Get]]/[[Set]]
let map = new Map()
let proxy = new Proxy(map, {})
proxy.set('item', 1) // Error
map通过 Map.prototype.set
方法,通过this [[MapData]]
希望将item存入,但是此时this = proxy,proxy 找不到它,实现不了。
同样,我们可以将函数调用的this绑定,绑定到原始map上,但是缺点就是 函数会将 未包装的原始对象传递到别处,上面我们已经说了。
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);
alert(proxy.get('test')); // 1(工作了!)
Array代理正常
因为是很早之前的属性类型,没有使用内部插槽,push等都会被set拦截。
类私有字段不行
以为私有字段也是通过内部插槽实现。同样使用bind(原始对象)
可撤销的Proxy
一个可以被随时关闭的代理,关闭后,代理对象不再可用
通过 Proxy.revocable(target, handler)
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// 将 proxy 传递到其他某处,而不是对象...
alert(proxy.data); // Valuable data
// 稍后,在我们的代码中
revoke();
// proxy 不再工作(revoked)
alert(proxy.data); // Error Cannot perform 'get' on a proxy that has been revoked