快速掌握ES6的Proxy、Reflect

web前端开发

共 11302字,需浏览 23分钟

 · 2021-05-16

来源 | https://segmentfault.com/a/1190000039956559


前言

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红宝书第四版。


学习更多技能

请点击下方公众号


浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报