ES6精华:Proxy & Reflect

导语

本文主要介绍了ES6中ProxyReflect的精华知识,并附有恰当实例。Proxy意为代理器,通过操作为对象生成的代理器,实现对对象各类操作的拦截式编程。Reflect是一个包揽更为严格、健全的操作对象方法的模块。因为Proxy所能代理的方法和Reflect所包括的方法基本对应,而且在拦截方法里应该使用对应的Reflect方法返回结果,所以将两者合并在一起分享。

1 Proxy

1.1 登堂

先想个问题,如何管控对象某一属性的读取和修改(不涉及闭包创建私有属性)?
先创建不应被直接改动的容器属性:_属性名,再设置相应的settergetter函数或创建相应的操作方法。

--- 设置 setter 和 getter 函数
let obj = {
  _id: undefined,
  get id() {
    return this._id;
  },
  set id(v) {
    this._id = v;
  }
};
obj.id = 3; // 相当:obj._id = 3
console.log(obj.id); // console.log(obj._id);

--- 创建获取及修改方法
let obj = {
  _id: undefined,
  id() {
    if (!arguments.length) return this._id;
    this._id = arguments[0];
  }
};
obj.id(3); // 相当:obj._id = 3
console.log(obj.id()); // console.log(obj._id);

这样有明显的缺陷:要为每个需要管控的属性进行重复设置,而且实际上容器属性可以被任意修改。
如果要求升级,我们需要监听查看、删除、遍历对象属性的操作,怎么办?ES6之前只能凉拌,ES6之后Proxy代你办。

let obj = { id: 0, name: 'Wmaker' };
let objProxy = new Proxy(obj, {
  get(target, attr) {
    console.log(`Get ${attr}`);
    return target[attr];
  },
  set(target, attr, val) {
    console.log(`Set ${attr}`);
    target[attr] = val;
    return true;
  }
});
objProxy.id; // 打印出:Get id,相当:obj.id;
objProxy.name; // 打印出:Get name,相当:obj.name;

1.2 入室

如前节示例可知,Proxy是生成代理器的构造函数。传入的第一个参数为需要被代理的对象,第二个参数是需要拦截操作的配置对象(之后会列出所有可拦截的操作及其意义和注意事项)。配置对象中的每个拦截操作,都有默认格式的传入参数,有些也要求有特定的返回值(下面会罗列出某些规律)。

生成的代理器是一个与被代理对象关联的代理实例,可以像操作普通对象一样对待它。即是说,可以被delete掉某个属性,可以被遍历或获取原型。所有作用于代理器的操作,都相当直接作用于被代理对象,还可为其配置拦截操作。说的激愤点,苍老师能做好的,我们的小泽老师怎么会差呢?另外可代理的不单单是对象,属于对象的函数、数组都是无条件接受的。

为对象生成代理器之后,依然可以操作原对象,但这自然是不建议的。

参数
不同拦截函数的传入参数不尽相同,但一般为被代理对象,该操作需要的参数等和代理器对象。
不必记忆每个拦截函数的参数,为脑瓜减减负担,使用时先打印出arguments查看便会一目了然。

返回值
在拦截方法里,应尽量使用Reflect对应的方法进行操作,并返回该方法的返回值。一方面是简单,更重要的是在不同方法或某些环境下,对返回值有硬性的要求,否则直接报错。比如construct()必须返回一个对象,否则报错。再比如set()在严格模式下,必须返回true或可转化成true的值,无论操作是否成功。

"use strict";

--- 错误的做法
let obj = { id: 0 };
let objProxy = new Proxy(obj, {
  set(target, attr, val) {
    console.log(`Set ${attr}`);
    return target[attr] = val;
  }
});

objProxy.id = 1; // 操作成功。
objProxy.id = 0; // 操作已经成功,但报错,不再往下执行。

--- 推荐的做法
let obj = { id: 0 };
let objProxy = new Proxy(obj, {
  set(target, attr, val) {
    console.log(`Set ${attr}`);
    return Reflect.set(target, attr, val);
  }
});

objProxy.id = 1; // 操作成功。
objProxy.id = 0; // 操作成功。

拦截方法的返回值并不会直接反映到外部,JS会进行某些验证和排除。
比如即便在ownKeys()中返回全部的值,但实际到外部的只有相应的系列。

两次打印的结果不相等。

let obj = { id: 0, [Symbol.iterator]() {} };
let objProxy = new Proxy(obj, {
  ownKeys(target) {
    let res = Reflect.ownKeys(target);
    console.log('1', res);
    return res;
  }
});

console.log('2', Object.keys(objProxy));

限制性的延续
当被代理对象自身已有某些限制,比如不可扩展或属性不可写不可配置等。对象本身的操作已经受到了限制,这时如果执行相应的代理操作,自然会报错。比如当属性不可写时,如果代理并执行了set()操作,则会直接报错。

let obj = { id: 0 };
Object.defineProperty(obj, 'name', {
  value: 'Wmaker'
});

let objProxy = new Proxy(obj, {
  set(target, attr, val) {
    return Reflect.set(target, attr, val);
  }
});

objProxy.id = 1; // 操作成功。
objProxy.name = 'Limo'; // 报错。

this
有些原生对象的内部属性或方法,只有通过正确的this才能访问,所以无法进行代理。
比如日期对象,new Proxy(new Date(), {}).getDate(),报错提示:这不是个Date对象。

也有变通的办法,比如对于需要正确 this 的方法可以这样做。

let p = new Proxy(new Date(), {
  get(target, attr) {
    const v = Reflect.get(target, attr);
    return typeof v === 'function'
      ? v.bind(target)
      : v;
  }
});
p.getTime(); // 没有错误。

处于配置对象中的this直接指向配置对象,不是被代理对象或代理器对象。
处于被代理对象中的this,分为存在于方法和存在于getter/setter中两种。两者获取this的方式不同,我们以实例说明。

--- 例一,没有 set 拦截操作。
let obj = {
  get id() {
    console.log('o', this);
  },
  fn() {
    console.log('o', this);
  }
};

let objProxy = new Proxy(obj, {});

objProxy.id; // 打印出 objProxy 。
objProxy.fn(); // 打印出 objProxy 。


--- 例二,有 set 拦截操作。实际使用了 target[attr] 获取属性值。
let obj = {
  get id() {
    console.log('o', this);
  },
  fn() {
    console.log('o', this);
  }
};

let objProxy = new Proxy(obj, {
  get(target, attr) {
    console.log('p', this);
    return target[attr];
  }
});

objProxy.id;
// 打印出配置对象和 obj。
// 因为实际是通过被代理对象即 target 访问到 id 值的。

objProxy.fn();
// 打印出配置对象和 objProxy。
// 可以等价的将方法转化成 (objProxy.fn).call(objProxy)。
// 虽然方法也是通过 target 访问到的,但对于方法来说,环境变量一开始就确定了。

原型为代理器
如果对象的原型为代理器,当操作进行到原型时,实际是操作原型的代理器对象。这时,其行为和普通代理器一致。

let obj = Object.create(new Proxy({}, {
  get(target, attr) {
    console.log('In proxy.');
    return Reflect.get(target, attr);
  }
}));

obj.name; // 打印出 In proxy. 。
// 当在实例上找不到对应属性时,转到了原型上,这时便被拦截了。

1.3 代理类别

这里仅仅是罗列出某些重点,详细的请看手册(标准和行为处于变动中)。

get
拦截属性的读取操作,包括数组取值。

set
拦截属性的赋值操作,包括数组赋值。
严格模式下,必须返回可转化为true的值。
严格模式下,如果代理对象有某些限制(属性不可写等),执行相应的拦截操作自然会报错。

apply
拦截函数的调用、callapply操作。

has
拦截判断对象是否具有某属性。
只对inReflect.has()方法有效,for in属于遍历系列。

construct
拦截new命令,必须返回一个对象。

deleteProperty
拦截delete属性操作。
严格模式下,必须返回可转化为true的值。
严格模式下,如果代理对象有某些限制(属性不可写等),执行相应的拦截操作自然会报错。

defineProperty
拦截Object.defineProperty(),不会拦截defineProperties
严格模式下,如果代理对象有某些限制(属性不可配置等),执行相应的拦截操作自然会报错。

getOwnPropertyDescriptor
拦截Object.getOwnPropertyDescriptor(),不会拦截getOwnPropertyDescriptors
必须返回一个对象或undefined,即返回与原方法相同的返回值,否则报错。

getPrototypeOf
拦截获取对象原型,必须返回一个对象或者null
如果对象不可扩展,必须返回真实的原型对象。
直接访问__proto__,通过各类方法等等都会触发拦截。

setPrototypeOf
拦截Object.setPrototypeOf()方法。

isExtensible
拦截Object.isExtensible()操作。
返回值必须与目标对象的isExtensible属性保持一致,否则会抛出错误。

preventExtensions
拦截Object.preventExtensions(),返回值会自动转成布尔值。

ownKeys
拦截对象自身属性的遍历操作。
比如keys(),getOwnPropertyNames(),getOwnPropertySymbols()等等。
返回值必须是数组,项只能是字符串或Symbol,否则报错。
返回值会根据调用方法的需求进行过滤,比如Object.keys()里不会有symbole
如果目标对象自身包含不可配置的属性,则该属性必须被返回,否则报错。
如果目标对象不可扩展,返回值必须包含原对象的所有属性,且不能包含多余的属性,否则报错。

1.4 Proxy.revocable

此静态方法也用于生成代理器对象的,但它还会返回一个回收代理器的方法。
使用场景:不允许直接访问目标对象,必须通过代理访问。而且一旦访问结束,就收回代理权,不允许再次访问。

let {proxy, revoke} = Proxy.revocable(obj, {});
revoke();
此时其内部属性值 [[IsRevoked]] 为 true,不能再操作代理器,否则报错。

2 Reflect

2.1 作用

最终目的是成为语言内部方法的宿主对象。
比如说defineProperty, getPrototypeOf, preventExtensions都很明显属于内部方法,不应挂在Object下。

提供替代命令式操作的相应函数。
使用Reflect.has(obj, attr)替代in操作
使用Reflect.deleteProperty(obj, attr)替代delete操作

使函数的行为更加完善和严格。
在无法定义属性时,Reflect.defineProperty返回false而不是抛出错误。
在要求类型是对象的参数为非对象时,会直接报错而不是调用Object()进行转化。

Porxy可拦截的方法对应,方便在实现自定义行为时,直接调用以完成默认行为。

2.2 静态方法

这里仅仅是罗列出某些重点,详细的请看手册

Reflect.get
Reflect.get(obj, attr, receiver)
返回属性值,没有则返回undefined
receiver是设置gettersetter里的this指向,默认指向obj

Reflect.set
Reflect.set(obj, attr, value, receiver)
设置属性值,返回布尔值。
注意:当该属性不是getter时,传入receiver等同于设置receiver对象上的属性值。

Reflect.has
Reflect.has(obj, attr)
等同attr in obj

Reflect.deleteProperty
Reflect.deleteProperty(obj, attr)
等同delete obj[attr]

Reflect.construct
Reflect.construct(func, args)
args等同于使用apply方法传入的参数数组。
提供了不使用new来调用构造函数的方法,等同new func(...args)

Reflect.getPrototypeOf
作用及参数等同Object.getPrototypeOf(obj)

Reflect.setPrototypeOf
作用及参数等同Object.setPrototypeOf(obj, newProto)

Reflect.apply
作用及参数等同Function.prototype.apply.call(func, thisArg, args)

Reflect.defineProperty
作用及参数等同Object.defineProperty(obj, attr, descriptor)

Reflect.getOwnPropertyDescriptor
作用及参数等同Object.getOwnPropertyDescriptor(obj, attr)

Reflect.isExtensible
Reflect.isExtensible(obj)
返回一个布尔值,表示当前对象是否可扩展。

Reflect.preventExtensions
Reflect.preventExtensions(obj)
设置对象为不可扩展,返回布尔值。

Reflect.ownKeys
Reflect.ownKeys(obj)
返回对象本身的所有属性。
等同于Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和。

    原文作者:wmaker
    原文地址: https://segmentfault.com/a/1190000015581013
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞