Proxy
一个 Proxy 对象可以包装另一个对象,并且拦截诸如读取,写入等操作。
语法
let proxy = new Proxy(target, handler)// target 是要包装的对象,可以是任何东西,包括函数// hander是代理的配置,是一个对象,在这里配置对目标对象的读写等操作的拦截
let target = {}let proxy = new Proxy(target, {}) // 代理对象proxy,但是是个空代理,透明转发对原始对象的操作proxy.test = 5;target.test // 5proxy.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] = valreturn 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, ageObject.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']},// 不管三七二十一,可遍历配置上truegetOwnPropertyDescriptor(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` 方法,可能会将原始对象传递出去```javascriptlet 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); // truealert(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 // sayHisayHi.length // 1sayHi = delay(sayHi, 2000)sayHi.name // ''sayHi.length // 0sayHi('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 // sayHisayHi.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` 更好。```javascriptlet 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 = adminreturn Reflect.get(target, prop, receiver); // (*)}});
receiver保留了正确的this引用,当 admin.name 时,this就是 admin 。
更简单的写法
let userProxy = new Proxy(user, {get(...args) { // receiver = adminreturn 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
