ES6-Proxy

115 阅读4分钟

1. 基础

Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

本质:  修改的是程序默认行为,相当于在编程语言层面上做修改,属于元编程(Meta Programming)

1.1. 用法

const p = new Proxy(target, handler)

target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler: 一个通常以函数作为属性的对象,函数可以自定义在执行操作时对应的行为。

const obj = {}  
const handler = {  
  get(target, property, receiver) {  
    // do something  
    return Reflect.get(...arguments)  
  }  
}
const p = new Proxy(obj, handler)  
  
console.log(p.a)

使用代理的主要目的是可以定义捕获器(trap)。

  • 捕获器就是在处理程序对象中定义的“基本操作的拦截器”。
  • 每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作。
  • 每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前,先调用捕获器函数,从而拦截并修改相应的行为
  • 只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
const dinner = {  
  meal: 'tacos'  
}  
  
const handler = {  
  get(target, property) {  
    console.log('intercepted!')  
    return target[property]  
  }  
}  
  
const proxy = new Proxy(dinner, handler)  
console.log(proxy.meal)  
  
// intercepted!  (拦截)
// tacos
const handler = {  
  get: function(obj, prop) {  
    return prop in obj ? obj[prop] : 37;  
  }  
};  
const obj = { a: 'a' }  
const p = new Proxy(obj, handler);  
p.a = 1;  
p.b = 'b';  
p.c = undefined  
  
console.log(obj)  
console.log(p.a, p.b, p.c);        
console.log('d' in p, p.d);  
console.log('d' in obj, obj.d)  
console.log(obj == p)  
  
// { a: 1, b: 'b', c: undefined }  
// 1, b, undefined  
// false, 37  
// false, undefined  
// false

1.2. 捕获器介绍

捕获器(trap)是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。

13个Trap,与Reflect的13个方法一一对应。Trap可以看做是相应操作的钩子。

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。

  1. get(target, propKey, receiver):拦截读取对象属性的操作,返回读取到的值
  • 访问属性: proxy[foo]proxy.bar
  • 访问原型链上的属性: Object.create(proxy)[foo]
  • Reflect.get()
  1. set(target,propKey,value,receiver):拦截对象属性的设置操作,返回应当返回一个布尔值,设置成功返回true
  • 指定属性值: proxy[foo] = bar proxy.foo = bar
  •  指定继承者的属性值: Object.create(proxy)[foo] = bar
  •  Reflect.set()
  1. has(target,propKey):拦截propKey in proxy等查询操作,返回一个布尔值
  • 属性查询: foo in proxy
  • 继承属性查询: foo in Object.create(proxy)
  • with 检查 : with(proxy) { (foo); }
  • Reflect.has()
  1. deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • 删除属性: delete proxy[foo] 和 delete proxy.foo*
  • Reflect.deleteProperty()
  1. ownKeys(target):拦截Object.keys(proxy)、for...in等遍历操作,返回一个数组
  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()与for...in
  • Reflect.ownKeys()

6. getOwnPropertyDescriptor => Object.getOwnPropertyDescriptor()

7. defineProperty => Object.defineProperty()

8. preventExtensions => Object.preventExtensions()

9. getPrototypeOf => Object.getPrototypeOf()

10. isExtensible => Object.isExtensible()

11. setPrototypeOf => Object.setPrototypeOf()

12. apply(target, object, args):拦截 Proxy 实例作为函数调用的操作

13. construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,new

1.3 可撤销代理

中断代理对象与目标对象之间的联系,该撤销操作不可逆。

使用 Proxy.revocable()

const target = { name: 'vuejs'}
const {proxy, revoke} = Proxy.revocable(target, {})

console.log(proxy.name) // 正常取值输出 vuejs

revoke() // 取值完成对proxy进行封闭,撤消代理

proxy.name = 3 // TypeError: Cannot perform 'set' on a proxy that has been revoked

2. 编程模式

2.1 隐藏属性

通过拦截get、set,来防止外部获取、修改对象内部的某些属性。

const targetObject = {  
  age: 18,  
  _age: 32,  
  state: 'fighting',  
  _state: 'loafing'  
};  
const proxy = new Proxy(targetObject, {  
  get(target, property) {  
    if (property.startsWith('_')) {  
      return undefined;  
    } else {  
      return Reflect.get(...arguments);  
    }  
  },  
  has(target, property) {  
    if (property.startsWith('_')) {  
      return false;  
    } else {  
      return Reflect.has(...arguments);  
    }  
  }  
});  
// get()  
console.log(proxy._state); // undefined  
console.log(proxy.state); // 'loafing'  
// has()  
console.log('_state' in proxy); // false  
console.log('state' in proxy); // true

2.2 属性校验

set时,根据所赋的值决定是否允许赋值

下面的代码实现只允许为属性赋予number类型的值

const target = {  
  onlyNumbers: 0  
}  
const proxy = new Proxy(target, {  
  set(target, property, value) {  
    if(typeof value !== 'number') {  
      return false;  
    } else {  
      return Reflect.set(...arguments)  
    }  
  }  
})  
proxy.onlyNumbers = 1  
console.log(proxy.onlyNumbers) // 1  
proxy.onlyNumbers = '22'  
console.log(proxy.onlyNumbers)  // 1

2.3 观察者模式

const queuedObservers = new Set();  

const observe = fn => queuedObservers.add(fn);  

const observable = obj => new Proxy(obj, {  
  set(target, key, value, receiver) {  
    if(value === target[key]) return;  
    const result = Reflect.set(target, key, value, receiver); 
    // 遍历调用集合中的函数
    queuedObservers.forEach(observer => observer());  
    return result;  
  }  
});  
  
const person = observable({  
  name: '张三',  
  age: 20  
});  

const watchPerson = () => {  
  console.log(`修改后的人物信息:${person.name}, ${person.age}`) 
}  
observe(watchPerson);  // 将函数添加入集合

// 会触发set拦截器
person.name = '李四';  
person.age = 19;  
  
// 修改后的人物信息:李四, 20  
// 修改后的人物信息:李四, 19

3. 其他

3.1 Trap Invariant

每个Trap都可以intercept特定操作。属性若是不变式(Invariant),当该Trap改变默认行为时,抛出TypeError。

Trap Get的约束

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
  • 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined。
var obj = {};  
Object.defineProperty(obj, "a", {  
  configurable: false,  
  enumerable: false,  
  value: 10,  
  writable: false  
});  
  
var p = new Proxy(obj, {  
  get: function(target, prop) {  
    return 20;  
  }  
});  
  
p.a; //会抛出TypeError  
// 'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'undefined' but got '20')