前言

ES6新增的代理反射为开发者提供了拦挡并向基本操作嵌入额定行为的能力。具体地说,能够给指标对象定义一个关联的代理对象,而这个代理对象能够作为形象的指标对象来应用。在对指标对象的各种操作影响指标对象之前,能够在代理对象中对这些操作加以控制。

Proxy (代理)

代理是应用 Proxy 构造函数创立的。这个构造函数接管两个参数:指标对象和处理程序对象。短少其中任何一个参数都会抛出 TypeError。

创立空代理

如上面的代码所示,在代理对象上执行的任何操作实际上都会利用到指标对象。惟一可感知的不同
就是代码中操作的是代理对象。

const target = {  id: 'target' }; const handler = {}; const proxy = new Proxy(target, handler); // id 属性会拜访同一个值console.log(target.id); // target console.log(proxy.id); // target// 给指标属性赋值会反映在两个对象上// 因为两个对象拜访的是同一个值target.id = 'foo'; console.log(target.id); // foo console.log(proxy.id); // foo// 给代理属性赋值会反映在两个对象上// 因为这个赋值会转移到指标对象proxy.id = 'bar'; console.log(target.id); // bar console.log(proxy.id); // bar

定义捕捉器

捕捉器能够了解为处理程序对象中定义的用来间接或间接在代理对象上应用的一种“拦截器”,每次在代理对象上调用这些基本操作时,代理能够在这些操作流传到指标对象之前先调用捕捉器函数,从而拦挡并批改相应的行为。

const target = {  foo: 'bar' };const handler = {  // 捕捉器在处理程序对象中以办法名为键 get() {  return 'handler override';  } };const proxy = new Proxy(target, handler); console.log(target.foo); // bar console.log(proxy.foo); // handler override

get() 捕捉器会接管到指标对象,要查问的属性和代理对象三个参数。咱们能够对上述代码进行如下革新

const target = {  foo: 'bar' };const handler = {  // 捕捉器在处理程序对象中以办法名为键 get(trapTarget, property, receiver) {  console.log(trapTarget === target);  console.log(property);  console.log(receiver === proxy);  return trapTarget[property] } };const proxy = new Proxy(target, handler); proxy.foo; // true // foo // trueconsole.log(proxy.foo); // bar console.log(target.foo); // bar

处理程序对象中所有能够捕捉的办法都有对应的反射(Reflect)API 办法。这些办法与捕捉器拦挡的办法具备雷同的名称和函数签名,而且也具备与被拦挡办法雷同的行为。因而,应用反射 API 也能够像上面这样定义出空代理对象:

const target = {  foo: 'bar' }; const handler = {  get() {      // 第一种写法     return Reflect.get(...arguments);      // 第二种写法     return Reflect.get } }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // bar console.log(target.foo); // bar

咱们也能够以此,来对将要拜访的属性的返回值进行润饰。

const target = {  foo: 'bar',  baz: 'qux' }; const handler = {  get(trapTarget, property, receiver) {  let decoration = '';  if (property === 'foo') {  decoration = ' I love you';  }  return Reflect.get(...arguments) + decoration;  } }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // bar I love you console.log(target.foo); // bar console.log(proxy.baz); // qux console.log(target.baz); // qux

可撤销代理

有时候可能须要中断代理对象与指标对象之间的分割。对于应用 new Proxy()创立的一般代理来说,这种分割会在代理对象的生命周期内始终继续存在。Proxy 也裸露了 revocable()办法,这个办法反对撤销代理对象与指标对象的关联。撤销代理的操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的后果都一样。撤销代理之后再调用代理会抛出 TypeError。

const target = {  foo: 'bar' }; const handler = {  get() {  return 'intercepted';  } }; const { proxy, revoke } = Proxy.revocable(target, handler); console.log(proxy.foo); // intercepted console.log(target.foo); // bar revoke(); console.log(proxy.foo); // TypeError

代理另一个代理

代理能够拦挡反射 API 的操作,而这意味着齐全能够创立一个代理,通过它去代理另一个代理。这样就能够在一个指标对象之上构建多层拦挡网:

const target = {  foo: 'bar' }; const firstProxy = new Proxy(target, {  get() {  console.log('first proxy');  return Reflect.get(...arguments);  } }); const secondProxy = new Proxy(firstProxy, {  get() {  console.log('second proxy');  return Reflect.get(...arguments);  } }); console.log(secondProxy.foo); // second proxy // first proxy // bar

代理的问题与有余

1. 代理中的this

const target = {  thisValEqualsProxy() {  return this === proxy;  } } const proxy = new Proxy(target, {}); console.log(target.thisValEqualsProxy()); // false console.log(proxy.thisValEqualsProxy()); // true

这样看起来并没有什么问题,this指向调用者。然而如果指标对象依赖于对象标识,那就可能碰到意料之外的问题。

const wm = new WeakMap(); class User {  constructor(userId) {      wm.set(this, userId);  }  set id(userId) {      wm.set(this, userId);  }  get id() {      return wm.get(this);  } }const user = new User(123); console.log(user.id); // 123 const userInstanceProxy = new Proxy(user, {}); console.log(userInstanceProxy.id); // undefined

这是因为 User 实例一开始应用指标对象作为 WeakMap 的键,代理对象却尝试从本身获得这个实
例。要解决这个问题,就须要重新配置代理,把代理 User 实例改为代理 User 类自身。之后再创立代
理的实例就会以代理实例作为 WeakMap 的键了:

const UserClassProxy = new Proxy(User, {}); const proxyUser = new UserClassProxy(456); console.log(proxyUser.id);

2. 代理与外部槽位

在代理Date类型时:依据 ECMAScript 标准,Date 类型办法的执行依赖 this 值上的外部槽位[[NumberDate]]。代理对象上不存在这个外部槽位,而且这个外部槽位的值也不能通过一般的 get()和 set()操作拜访到,于是代理拦挡后本应转发给指标对象的办法会抛出 TypeError:

const target = new Date(); const proxy = new Proxy(target, {}); console.log(proxy instanceof Date); // true proxy.getDate(); // TypeError: 'this' is not a Date object

Reflect(反射)

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect的设计目标:

  1. 将Object对象的一些显著属于语言外部的办法(比方Object.defineProperty),放到Reflect对象上。
  2. 批改某些Object办法的返回后果,让其变得更正当。比方,Object.defineProperty(obj, name, desc)在无奈定义属性时,会抛出一个谬误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  3. 让Object操作都变成函数行为。某些Object操作是命令式,比方name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  4. Reflect对象的办法与Proxy对象的办法一一对应,只有是Proxy对象的办法,就能在Reflect对象上找到对应的办法。这就让Proxy对象能够不便地调用对应的Reflect办法,实现默认行为,作为批改行为的根底。也就是说,不论Proxy怎么批改默认行为,你总能够在Reflect上获取默认行为。

代理与反射API

get()

接管参数:

  • target:指标对象。
  • property:援用的指标对象上的字符串键属性。
  • receiver:代理对象或继承代理对象的对象。
    返回:
  • 返回值无限度
    get()捕捉器会在获取属性值的操作中被调用。对应的反射 API 办法为 Reflect.get()。
const myTarget = {}; const proxy = new Proxy(myTarget, {  get(target, property, receiver) {  console.log('get()');  return Reflect.get(...arguments)  } }); proxy.foo; // get()

set()

接管参数:

  • target:指标对象。
  • property:援用的指标对象上的字符串键属性。
  • value:要赋给属性的值。
  • receiver:接管最后赋值的对象。
    返回:
  • 返回 true 示意胜利;返回 false 示意失败,严格模式下会抛出 TypeError。

set()捕捉器会在设置属性值的操作中被调用。对应的反射 API 办法为 Reflect.set()。

const myTarget = {}; const proxy = new Proxy(myTarget, {  set(target, property, value, receiver) {  console.log('set()');  return Reflect.set(...arguments)  } }); proxy.foo = 'bar'; // set()

has()

接管参数:

  • target:指标对象。
  • property:援用的指标对象上的字符串键属性。

返回:

  • has()必须返回布尔值,示意属性是否存在。返回非布尔值会被转型为布尔值。

has()捕捉器会在 in 操作符中被调用。对应的反射 API 办法为 Reflect.has()。

const myTarget = {}; const proxy = new Proxy(myTarget, {  has(target, property) {  console.log('has()');  return Reflect.has(...arguments)  } }); 'foo' in proxy; // has()

defineProperty()

Reflect.defineProperty办法根本等同于Object.defineProperty,用来为对象定义属性。

接管参数:

  • target:指标对象。
  • property:援用的指标对象上的字符串键属性。
  • descriptor:蕴含可选的 enumerable、configurable、writable、value、get 和 set定义的对象。

返回:

  • defineProperty()必须返回布尔值,示意属性是否胜利定义。返回非布尔值会被转型为布尔值。
const myTarget = {}; const proxy = new Proxy(myTarget, {  defineProperty(target, property, descriptor) {  console.log('defineProperty()');  return Reflect.defineProperty(...arguments)  } }); Object.defineProperty(proxy, 'foo', { value: 'bar' }); // defineProperty()

getOwnPropertyDescriptor()

Reflect.getOwnPropertyDescriptor根本等同于Object.getOwnPropertyDescriptor,用于失去指定属性的形容对象。

接管参数:

  • target:指标对象。
  • property:援用的指标对象上的字符串键属性。

返回:

  • getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回 undefined。
const myTarget = {}; const proxy = new Proxy(myTarget, {  getOwnPropertyDescriptor(target, property) {  console.log('getOwnPropertyDescriptor()');  return Reflect.getOwnPropertyDescriptor(...arguments)  } }); Object.getOwnPropertyDescriptor(proxy, 'foo'); // getOwnPropertyDescriptor()

deleteProperty()

Reflect.deleteProperty办法等同于delete obj[name],用于删除对象的属性。

接管参数:

  • target:指标对象。
  • property:援用的指标对象上的字符串键属性。

返回:

  • deleteProperty()必须返回布尔值,示意删除属性是否胜利。返回非布尔值会被转型为布尔值。

ownKeys()

Reflect.ownKeys办法用于返回对象的所有属性,根本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。

接管参数:

  • target:指标对象。

返回:

  • ownKeys()必须返回蕴含字符串或符号的可枚举对象。

getPrototypeOf()

Reflect.getPrototypeOf办法用于读取对象的__proto__属性

接管参数:

  • target:指标对象。

返回:

  • getPrototypeOf()必须返回对象或 null。

等等。。

代理模式

跟踪属性拜访

通过捕捉 get、set 和 has 等操作,能够晓得对象属性什么时候被拜访、被查问。把实现相应捕捉器的某个对象代理放到利用中,能够监控这个对象何时在何处被拜访过:

const user = {  name: 'Jake' }; const proxy = new Proxy(user, {  get(target, property, receiver) {  console.log(`Getting ${property}`);  return Reflect.get(...arguments);  },  set(target, property, value, receiver) {  console.log(`Setting ${property}=${value}`);  return Reflect.set(...arguments);  } }); proxy.name; // Getting name proxy.age = 27; // Setting age=27

暗藏属性

代理的外部实现对外部代码是不可见的,因而要暗藏指标对象上的属性也轻而易举。

const hiddenProperties = ['foo', 'bar']; const targetObject = {  foo: 1,  bar: 2,  baz: 3 }; const proxy = new Proxy(targetObject, {  get(target, property) {  if (hiddenProperties.includes(property)) {  return undefined;  } else {  return Reflect.get(...arguments);  }  },  has(target, property) {  if (hiddenProperties.includes(property)) {  return false;  } else {  return Reflect.has(...arguments);  }  } }); // get() console.log(proxy.foo); // undefined console.log(proxy.bar); // undefined console.log(proxy.baz); // 3 // has() console.log('foo' in proxy); // false console.log('bar' in proxy); // false console.log('baz' in proxy); // true

属性验证

因为所有赋值操作都会触发 set()捕捉器,所以能够依据所赋的值决定是容许还是回绝赋值:

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

函数与结构函数参数验证

跟爱护和验证对象属性相似,也可对函数和结构函数参数进行审查。比方,能够让函数只接管某种类型的值:

function median(...nums) {      return nums.sort()[Math.floor(nums.length / 2)]; } const proxy = new Proxy(median, {      apply(target, thisArg, argumentsList) {          for (const arg of argumentsList) {              if (typeof arg !== 'number') {                  throw 'Non-number argument provided';              }          }  return Reflect.apply(...arguments);  } }); console.log(proxy(4, 7, 1)); // 4 console.log(proxy(4, '7', 1)); // Error: Non-number argument provided 相似地,能够要求实例化时必须给构造函数传参:class User {  constructor(id) {      this.id_ = id;  } } const proxy = new Proxy(User, {  construct(target, argumentsList, newTarget) {      if (argumentsList[0] === undefined) {          throw 'User cannot be instantiated without id';      } else {          return Reflect.construct(...arguments);      }  } }); new proxy(1); new proxy(); // Error: User cannot be instantiated without id

数据绑定与可察看对象

通过代理能够把运行时中本来不相干的局部分割到一起。这样就能够实现各种模式,从而让不同的代码互操作。比方,能够将被代理的类绑定到一个全局实例汇合,让所有创立的实例都被增加到这个汇合中:

const userList = []; class User {  constructor(name) {  this.name_ = name;  } } const proxy = new Proxy(User, {  construct() {  const newUser = Reflect.construct(...arguments);  userList.push(newUser);  return newUser;  } }); new proxy('John'); new proxy('Jacob'); new proxy('Jingleheimerschmidt'); console.log(userList); // [User {}, User {}, User{}]

另外,还能够把汇合绑定到一个事件分派程序,每次插入新实例时都会发送音讯:

const userList = []; function emit(newValue) {  console.log(newValue); } const proxy = new Proxy(userList, {  set(target, property, value, receiver) {  const result = Reflect.set(...arguments);  if (result) {  emit(Reflect.get(target, property, receiver));  }  return result;  } }); proxy.push('John'); // John proxy.push('Jacob'); // Jacob

应用 Proxy 实现观察者模式

const queuedObservers = new Set();const observe = fn => queuedObservers.add(fn);const observable = obj => new Proxy(obj, {set});function set(target, key, value, receiver) {  const result = Reflect.set(target, key, value, receiver);  queuedObservers.forEach(observer => observer());  return result;}const person = observable({  name: '张三',  age: 20});function print() {  console.log(`${person.name}, ${person.age}`)}observe(print);person.name = '李四';// 输入// 李四, 20

结尾

本文次要参考阮一峰es6教程、js红宝书第四版

因为自己程度无限,如有谬误,敬请与我分割指出,谢谢。