Proxy

一个 Proxy 对象可以包装另一个对象,并且拦截诸如读取,写入等操作。

语法

  1. let proxy = new Proxy(target, handler)
  2. // target 是要包装的对象,可以是任何东西,包括函数
  3. // hander是代理的配置,是一个对象,在这里配置对目标对象的读写等操作的拦截
  1. let target = {}
  2. let proxy = new Proxy(target, {}) // 代理对象proxy,但是是个空代理,透明转发对原始对象的操作
  3. proxy.test = 5;
  4. target.test // 5
  5. proxy.test // 5 代理对象中也可以读取
  6. for(let k in proxy) {console.log(k)} // test 迭代也正常工作

拦截内部方法

JS对于内部对象的操作,在JS规范中都有对应的内部方法,描述了最底层的工作方式。
image.png

内部方法和捕捉器必须满足条件,比如[[Set]]写入值成功,需要返回true,否则false

带有get捕捉器的默认值

get(target, property, receiver)

  • target,目标对象本身,也就是传递给new Proxy的
  • property,目标属性名
  • receiver,目标属性本身是个getter访问器属性,则该参数指向读取属性时所在的this对象,有点绕,后面例子看

例子:用get来实现一个对象的默认值,创建一个数组,对不存在的数组项返回0。

  1. let number = [0,1,2]
  2. number = new Proxy(number, {
  3. get(target, prop, receiver) {
  4. if(prop in target) {
  5. return target[prop]
  6. }
  7. return 0 // 返回默认值
  8. }
  9. })

注意:代理对象应覆盖掉原对象

对象一旦被代理,就不希望外部还能访问到换对象,因此用 obj = new Proxy(obj, {...}) 的方式覆盖原对象。

set捕捉器

set(target, prop, value, receiver)
前2个参数同 get

  • value,需要设置的新值
  • receiver,与get捕捉器一样,仅对 setter 访问器属性相关。

例子:我们来设计一个数组,仅支持写入 number

  1. let number = []
  2. number = new Proxy(number, {
  3. set(target, prop, val, receiver) {
  4. if(typeof val === 'number') {
  5. target[prop] = val
  6. return true
  7. }
  8. return false
  9. }
  10. })

ownKeys捕捉器和getOwnPropertyDescriptor捕捉器

这2个捕捉去对 Object.keys/values/entries for in 循环和大多数其他遍历对象的属性和方法都有用

这里回顾下,for in会循环原型链上带enumerable的非Symbol键

例子:过滤掉_开头的私有属性

  1. let user = {
  2. name: 'Jack',
  3. age: 30,
  4. _password: '123'
  5. }
  6. user = new Proxy(user, {
  7. ownKeys(target) {
  8. return Object.keys(target).filter(key => !key.startsWith('_'))
  9. }
  10. })
  11. for(let k in user) {console.log(k)} // name, age
  12. Object.values(user) // [Jack, 30]

Object.keys/values 等不会列出对象中不存在的键和值

  1. let user = {}
  2. user = new Proxy(user, {
  3. ownKeys(target) { // 这里期望调用Object.keys时,调用该拦截器,返回了['a', 'b']
  4. return ['a', 'b']
  5. }
  6. })
  7. // 但实际上不会如期望这样返回
  8. Object.keys(user); // []
  9. // Object.keys 会对每个属性调用内部方法 getOwnProperty 来获取属性描述符,如果没有enumerable,则忽略掉

所以要想让上面的代码返回 ['a', 'b'] ,要么赋值user提供a,b属性,要么使用拦截器,返回 enumerable: true

  1. let user = {}
  2. user = new Proxy(user, {
  3. ownKeys(target) {
  4. return ['a', 'b']
  5. },
  6. // 不管三七二十一,可遍历配置上true
  7. getOwnPropertyDescriptor(target, prop) {
  8. return {
  9. enumerable: true,
  10. configurable: true
  11. }
  12. }
  13. })
  14. Object.keys(user) // [a, b]

综合例子

_开头的属性,一般都是内部属性,不应该从对象外部访问它们。
借助代理对象,我们可以实现拦截对私有属性的访问等一系列操作。

  • get实现读取属性时抛错
  • set写入私有属性抛错
  • deleteProperty 删除属性时抛错
  • ownKeys 在使用遍历for in 或 Object.keys类似方法时,排除_开头 ```javascript let user = { name: ‘john’, checkPassword() {
    1. this._password; // user对象内部方法,应该访问到_password.
    }, _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(‘‘)); } })

  1. <a name="Wz9xz"></a>
  2. ### 不好的get实现
  3. 应对 对象调用 比如 user.checkPassword()调用,代理成功后,user变成了Proxy后的对象,此时访问的,this指向的是代理对象user,user(proxy)._password,触发get拦截器,被拦截了。所以需要checkPassword方法bind原始对象user,这样就可以访问_password,而不是通过代理对象,不会触发拦截。
  4. 但该方法有很大问题,因为方法可能会将未被代理的对象再传递到其他地方,使用起来就会混乱,到底用的是代理对象,还是非代理对象。<br />比如这个 `checkPassword` 方法,可能会将原始对象传递出去
  5. ```javascript
  6. let user = {
  7. checkPassword() {
  8. someFn(this)
  9. }
  10. }
  11. user = new Proxy(user, {
  12. get(target, prop) {
  13. let value = target[value]
  14. // 这里target是原user对象
  15. return typeof value === 'function' ? value.bind(target) : value
  16. }
  17. })
  18. user.checkPassword // this指向的是原始target对象,所以里面someFn(this),把原始未经过包装的user传递出去了

has捕捉器,漂亮的语法糖

  1. let range = {
  2. start: 1,
  3. end: 10
  4. };
  5. range = new Proxy(range, {
  6. has(target, prop) {
  7. return prop >= target.start && prop <= target.end;
  8. }
  9. });
  10. alert(5 in range); // true
  11. alert(50 in range); // false

包装函数+apply转发 proxy+apply转发

  1. // 一个常见的函数
  2. function delay(f, time) {
  3. return function(...args){
  4. setTimeout(() => {
  5. f.apply(this, args)
  6. }, time)
  7. }
  8. }
  9. function sayHi(name) {
  10. console.log(name)
  11. }
  12. // 未被包装前,还能访问函数名等
  13. sayHi.name // sayHi
  14. sayHi.length // 1
  15. sayHi = delay(sayHi, 2000)
  16. sayHi.name // ''
  17. sayHi.length // 0
  18. sayHi('jack') // 2s后打印jack

代码的业务需要达到了,但是包装后的sayHi,丢失了其原始函数的属性访问,如函数name,参数length。而 Proxy 则要强大的多。

apply/call 转发操作,在内部触发 [[Call]] 操作,可以通过 apply 拦截器来拦截,因此:

  1. function delay(f, t) {
  2. return new Proxy(f, {
  3. apply(target, thisArg, args) {
  4. setTimeout(() => {
  5. target.apply(thisArg, args)
  6. }, t)
  7. }
  8. })
  9. }
  10. function sayHi(name) {
  11. console.log(name)
  12. }
  13. sayHi = delay(sayHi, 2000)
  14. sayHi('jack') // 2s后打印
  15. sayHi.name // sayHi
  16. 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) } })

  1. <a name="h3YN3"></a>
  2. ## 差别,Reflect更好
  3. 大多数情况,我们可以使用 `target[prop]` 替代 `Reflect.get(target, prop, receiver)` 。但是他们也有细微差别。使用 `Reflect.get` 更好。
  4. ```javascript
  5. let user = {
  6. _name: 'Guest',
  7. get name() {return this._name} // 这是一个getter访问器属性
  8. }
  9. user = new Proxy(user, {
  10. get(target, prop, receiver) {
  11. return target[prop]
  12. }
  13. })
  14. user.name // Guest 没啥问题

但是如果你使用了继承,target的指向就有问题

  1. let admin = Object.create(user)
  2. admin._name = 'Admin'
  3. admin.name // Guest !!! 不是Admin吗?
  4. // 因为getter访问器里,this指向的是target(原始user)

因为这种情况,导致访问失败,而Reflect可以解决

  1. let userProxy = new Proxy(user, {
  2. get(target, prop, receiver) { // receiver = admin
  3. return Reflect.get(target, prop, receiver); // (*)
  4. }
  5. });

receiver保留了正确的this引用,当 admin.name 时,this就是 admin
更简单的写法

  1. let userProxy = new Proxy(user, {
  2. get(...args) { // receiver = admin
  3. return Reflect.get(...args); // (*)
  4. }
  5. });

Proxy的局限性

无法正常代理某些使用内部插槽的内建对象

一些内建对象,如 Map 等,使用了所谓的内部插槽(内部属性[[xx]]),内部有些实现,并不是通过Proxy的拦截器对应的内部方法来实现的。
比如Map,Map通过this[[MapData]],在内建方法中可以直接访问它们,而不是 [[Get]]/[[Set]]

  1. let map = new Map()
  2. let proxy = new Proxy(map, {})
  3. proxy.set('item', 1) // Error

map通过 Map.prototype.set 方法,通过this [[MapData]] 希望将item存入,但是此时this = proxy,proxy 找不到它,实现不了。
同样,我们可以将函数调用的this绑定,绑定到原始map上,但是缺点就是 函数会将 未包装的原始对象传递到别处,上面我们已经说了。

  1. let map = new Map();
  2. let proxy = new Proxy(map, {
  3. get(target, prop, receiver) {
  4. let value = Reflect.get(...arguments);
  5. return typeof value == 'function' ? value.bind(target) : value;
  6. }
  7. });
  8. proxy.set('test', 1);
  9. alert(proxy.get('test')); // 1(工作了!)

Array代理正常

因为是很早之前的属性类型,没有使用内部插槽,push等都会被set拦截。

类私有字段不行

以为私有字段也是通过内部插槽实现。同样使用bind(原始对象)

可撤销的Proxy

一个可以被随时关闭的代理,关闭后,代理对象不再可用
通过 Proxy.revocable(target, handler)

  1. let object = {
  2. data: "Valuable data"
  3. };
  4. let {proxy, revoke} = Proxy.revocable(object, {});
  5. // 将 proxy 传递到其他某处,而不是对象...
  6. alert(proxy.data); // Valuable data
  7. // 稍后,在我们的代码中
  8. revoke();
  9. // proxy 不再工作(revoked)
  10. alert(proxy.data); // Error Cannot perform 'get' on a proxy that has been revoked