Proxy

为什么需要 Proxy

回想一下在 ES6 之前,是如何监听对象的操作呢?

通过 Object.defineProperty() 来实现的。

回顾 Object.defineProperty

Object.defineProperty() 是干什么? 该方法用来精确控制对象的属性的。也被称为属性描述符。

属性描述符分为两种:

    1. 数据属性描述符(writable,value)
    1. 存取属性描述符(get,set)
 // 数据属性描述符
 const obj = {};
 Object.defineProperty(obj, "name", {
   writable: true,
   configurable: true,
   enumerable: true,
   value: "copyer",
 });// 存取属性描述符
 const obj = { _name: "copyer" };
 Object.defineProperty(obj, "name", {
   configurable: true,
   enumerable: true,
   get: function () {
     console.log("进入getter操作");
     return this._name;
   },
   set: function (newValue) {
     console.log("进入setter操作");
     this._name = newValue;
   },
 });

危险

从上面的代码就可以看出,get/set 与 writable/value 是不能共存的。

Object.defineProperty 监听

监听对象的操作:获取和设置

 const obj = {
   name: "copyer",
   age: 18,
 };
 ​
 Object.keys(obj).forEach((key) => {
   let value = obj[key];
   Object.defineProperty(obj, key, {
     configurable: true,
     enumerable: true,
     get: function () {
       console.log("监听操作:getter");
       return value;
     },
     set: function (newValue) {
       console.log("监听操作:setter");
       // obj[key] = value;
       // 这一步,犯错了,不停的触发setter操作,还是多练习,才知道自己的不足啊
       value = newValue;
     },
   });
 });
 ​
 obj.name = "kobe";
 console.log(obj.name)

结论

结论 使用 Object.defineProperty 的方法可以实现监听,进行逻辑操作(在getter和setter中进行),那么这种方式有什么不好吗?

  1. 改变了 Object.defineProperty的初衷,其初衷仅仅只是对象属性的描述符进行设置,现在在里面进行逻辑操作。
  2. 改变了属性的描述符。从默认的数据属性描述符转变成了存取属性描述符了,改变了属性的本意。
  3. 拦截的对象操作有限,只能对获取和设置进行拦截,其他操作(删除等)就无能为力了。
    那么,Proxy就诞生了,出现的目的就是为了解决上面的一些列问题。

Proxy的基本使用

在 ES6 中新增了一个Proxy类,从名字就可以看出,帮助我们创建一个代理的。
简单的来说,监听对一个对象的操作,可以先创建一个代理对象(Proxy对象)。
之后对该对象的操作,都是通过对代理对象操作来完成的,代理对象可以监听了对原来对象进行了哪些的操作,进行捕捉。
语法解析

new Proxy(target, handler)

// target: 目标对象
// handler: 捕捉器(进行拦截操作)
 const obj = {
   name: "copyer",
   age: 12,
 };const objProxy = new Proxy(obj, {});
 // objProxy 就是代理对象
 console.log(objProxy); //

其实用法还是很简单的。痛苦的地方就在于它有太多的捕捉器,高达13种。

Proxy的13种捕捉器

1、get 捕捉器(常用)

获取值的捕捉器
 const obj = {
    name: "copyer",
    age: 12,
 };const objProxy = new Proxy(obj, {
     /**
      * @param {*} target :目标对象
      * @param {*} key : 键值
      * @param {*} receiver :代理对象(后面会专门讲解)
      */
     get: function (target, key, receiver) {
       console.log("get捕捉器");
       return target[key];
     },
 });
 ​
 console.log(objProxy.name); // copyer

2、set 捕捉器(常用)

设置值的捕捉器
 const obj = {
   name: "copyer",
   age: 12,
 };const objProxy = new Proxy(obj, {
   /**
    * @param {*} target : 目标对象
    * @param {*} key :键值
    * @param {*} newValue :新增
    * @param {*} receiver :代理对象
    */
   set: function (target, key, newValue, receiver) {
     console.log("set捕捉器");
     target[key] = newValue;
   },
 });
 ​
 objProxy.age = 23;
 console.log(obj.age); // 23

3、has 捕捉器(常用)

in操作符的捕捉
 const obj = {
   name: "copyer",
   age: 12,
 };const objProxy = new Proxy(obj, {
   has: function (target, key) {
     console.log("has捕捉器");
     return Object.keys(target).includes(key);
   },
 });
 console.log("name" in objProxy);

4、deleteProperty 捕捉器(常用)

删除对象属性的拦截
 const obj = {
   name: "copyer",
   age: 12,
 };const objProxy = new Proxy(obj, {
   deleteProperty: function (target, key) {
     console.log("deleteProperty捕捉器");
     return delete target[key];
   },
 });
 ​
 console.log(delete objProxy.name); // true

5、getPrototypeOf 捕捉器

Object.getPrototypeOf()的方法捕捉器。

Object.getPrototypeOf()作用:得到对象的原型。

 const objProxy = new Proxy(obj, {
   getPrototypeOf: function (target) {
     console.log("getPrototypeOf捕捉器");
     return Object.getPrototypeOf(target);
   },
 });
 Object.getPrototypeOf(objProxy)

6、setPrototypeOf 捕捉器

Object.setPrototypeOf()的方法捕捉器。
Object.setPrototypeOf()的作用:设置对象的原型指向另外一个对象

 const objProxy = new Proxy(obj, {
  /**
    * @param {*} target : 目标对象
    * @param {*} newObj : 原型对象
    */
   setPrototypeOf: function (target, newObj) {
     console.log('setPrototypeOf捕捉器')
     return Object.setPrototypeOf(target, newObj);
   },
 });
 Object.getPrototypeOf(objProxy)

7、isExtensible 捕捉器

Object.isExtensible 方法的捕捉器
Object.isExtensible(): 判断对象是否可以扩展。

 const objProxy = new Proxy(obj, {
   isExtensible: function (target) {
     console.log("isExtensible捕获器");
     return Object.isExtensible(target);
   },
 });
 const res = Object.isExtensible(objProxy)  
 console.log(res)  // true// 如果被冻结了
 Object.freeze(objProxy)
 const res = Object.isExtensible(objProxy)
 console.log(res)  // false

8、preventExtensions 捕捉器

Object.preventExtensions 方法的捕捉器。
禁止扩展,在新增上跟冻结是一样的效果。
 const objProxy = new Proxy(obj, {
   preventExtensions: function (target) {
     console.log("preventExtensions捕捉器");
     return Object.preventExtensions(target);
   },
 });
 Object.preventExtensions(objProxy);
 ​
 objProxy.add = "123";
 console.log(objProxy); // { name: 'copyer', age: 12 } 没有被修改

9、getOwnPropertyDescriptor 捕捉器

Object.getOwnPropertyDescriptor 方法的捕捉器。

Object.getOwnPropertyDescriptor(): 获取属性描述符信息。

 const objProxy = new Proxy(obj, {
   getOwnPropertyDescriptor: function (target) {
     console.log("preventExtensions捕捉器");
     return Object.getOwnPropertyDescriptor(target);
   },
 });
 Object.getOwnPropertyDescriptor(objProxy);

10、defineProperty 捕捉器

Object.defineProperty方法的捕捉器。
 const objProxy = new Proxy(obj, {
   /**
    * @param {*} target : 目标对象
    * @param {*} key :键值
    * @param {*} obj :属性描述对象
    */
   defineProperty: function (target, key, obj) {
     console.log("defineProperty捕捉器");
     return Object.defineProperty(target, key, obj);
   },
 });
 Object.defineProperty(objProxy, 'name', {
   writable: true,
   configurable: true,
   enumerable: true,
   value: "fdafdsa",
 });
 ​
 console.log(objProxy); // { name: 'fdafdsa', age: 12 }

11、ownKeys 捕捉器

Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。

Object.getOwnPropertyNames: 获取对象的属性名(普通的字符串)
Object.getOwnPropertySymbols: 获取对象的属性名(symbol作为key值)

 const objProxy = new Proxy(obj, {
    ownKeys: function (target) {
     console.log("ownKeys捕捉器");
     return Object.getOwnPropertyNames(target);
   },
 });
 const res = Object.getOwnPropertyNames(objProxy);
 console.log(res) // [ 'name', 'age' ]

12、apply 捕捉器(特殊)

针对函数

函数调用操作的捕捉器。(普通方式的调用,并不只是针对apply调用)
 function foo() {
   console.log("foo");
 }const fooProxy = new Proxy(foo, {
   /**
    * @param {*} target 目标函数
    * @param {*} thisArg this
    * @param {*} paramsArr 传递的参数数组
    */
   apply: function (target, thisArg, paramsArr) {
     console.log('apply捕捉器')
     target.apply(thisArg, paramsArr);
   },
 });fooProxy()

13、construct 捕捉器(特殊)

针对函数

new 操作符的捕捉器。
 function foo() {
   console.log("foo");
 }const fooProxy = new Proxy(foo, {
   /**
    * @param {*} target 目标函数 
    * @param {*} paramsArr 传递参数
    * @returns 
    */
   construct: function (target, paramsArr) {
     console.log('construct捕捉器')
     return new target(...paramsArr)
   }
 });new fooProxy()

13种拦截器,其实理解很简单,就是量比较的大。

但是常用的只有四种,所以只需要记住 get、 set、 has、 deleteProperty即可,其他的翻阅资料就行。

Reflect

Reflect 是 ES6 新增的一个API,它是一个对象,字面的意思是反射。
MDN 对 Reflect 的解释:Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
知道两点即可:

Reflect是一个对象。
Reflect提供了拦截 JavaScript的操作方法。

Reflect的作用
Reflect 提供了很多操作 JavaScript 对象的方法,与 Object 操作对象有些类似。
比如:

Reflect.getPrototypeOf() 与 Object.getPrototypeOf() 类似
Reflect.defineProperty() 与 Object.defineProperty() 类似
...

到了这里,是不是有疑问?
既然有了 Object 提供了这些方法,为什么还需要 Reflect 的呢?

早期的ECMA规范中没有考虑到这么多,不知道如何设计对对象的操作更加的规范,所以将操作对象的API放在了Object上。
Object是一个构造函数,将这些API放到函数本身就不是很合理(虽然函数也是对象)。 还包含了一些 in ,delete操作符,使JavaScript看起来有点的奇怪。

基于上面的问题,Reflect 就是为了解决上面的一些列问题,是API设计看起来更加的规范。
MDN:对比Reflect与Object
看了MDN的对比之后,不能发现,有些方法 Object 含有,Reflect 没有;有些方法 Reflect 含有,但是 Object 没有。
所以说,Reflect 并不能完全替代 Object,只是在某一些方面是可以转变的。

Proxy 和 Reflect 的配合使用

在上面的Proxy实例中存在一定的问题,截取一段吧(get捕获器为例):

 const obj = {
   name: "copyer",
   age: 12,
 };const objProxy = new Proxy(obj, {
   get: function (target, key, receiver) {
     console.log("get捕捉器");
     return target[key];
   },
 });
 ​
 console.log(objProxy.name); // copyer

虽然创建了一个代理对象,在使用的过程中,也是对代理对象的操作。但是在捕捉的时候,还是操作了 target 对象,也就是所谓的目标对象,只是在原来的形式上多增加了一步。
那么 Reflect 就完美解决了这个问题。
Proxy有 13 种捕捉器,Reflect 就提供了 13 种操作对象的方法(全程一一对应)。
针对对象的11种捕捉器:

 const obj = {
   name: "copyer",
   age: 12,
 };const objProxy = new Proxy(obj, {
   get: function (target, key, receiver) {
     console.log("get捕获器");
     return Reflect.get(target, key);
   },
   set: function (target, key, newValue, receiver) {
     console.log("set捕获器");
     return Reflect.set(target, key, newValue);
   },
   has: function (target, key) {
     console.log("has捕获器");
     return Reflect.has(target, key);
   },
   deleteProperty: function (target, key) {
     console.log("deleteProperty捕获器");
     return Reflect.deleteProperty(target, key);
   },
   getPrototypeOf: function (target) {
     console.log("getPrototypeOf捕获器");
     return Reflect.getPrototypeOf(target);
   },
   setPrototypeOf: function (target, newObj) {
     return Reflect.setPrototypeOf(target, newObj);
   },
   isExtensible: function (target) {
     console.log("isExtensible捕获器");
     return Reflect.isExtensible(target);
   },
   preventExtensions: function (target) {
     console.log("preventExtensions捕捉器");
     return Reflect.preventExtensions(target);
   },
   getOwnPropertyDescriptor: function (target) {
     console.log("getOwnPropertyDescriptor捕捉器");
     return Reflect.getOwnPropertyDescriptor(target);
   },
   defineProperty: function (target, key, obj) {
     console.log("defineProperty捕捉器");
     return Reflect.defineProperty(target, key, obj);
   },
   ownKeys: function (target, key) {
     console.log("ownKeys捕捉器");
     return Reflect.ownKeys(target, key);
   },
 });

针对函数的2种捕捉器:

 function foo() {
   console.log("foo");
 }const fooProxy = new Proxy(foo, {
   apply: function (target, thisArg, paramsArr) {
     console.log("apply捕捉器");
     Reflect.apply(thisArg, paramsArr);
   },
   construct: function (target, paramsArr) {
     console.log("construct捕捉器");
     return new Reflect.construct(paramsArr);
   },
 });

是不是非常的nice,还非常的简洁。
至于 Reflect 对象上面的方法的参数,就自己去 MDN 去详细的了解吧,这里就不在做分析了。

MDN:Reflect

理解Reflect.construct()
这个函数调用,简单的来说,可以看成是一个 new 调用函数的操作。
具体语法:

 Reflect.construct(target, argumentsList[, newTarget])
 // 相当于 new Target(...argumentsList)

用法一:创建实例对象
 function Student(name, age) {
   this.name = name;
   this.age = age;
 }const stu1 = new Student("copyer", 18);
 const stu2 = Reflect.construct(Student, ["james", 35]);
 ​
 console.log(stu1); // Student { name: 'copyer', age: 18 }
 console.log(stu2); // Student { name: 'james', age: 35 }

用法二:借用函数体,创建其他的实例对象

 function Student(name, age) {
   this.name = name;
   this.age = age;
 }function Teacher() {}const stu2 = Reflect.construct(Student, ["james", 35], Teacher);
 ​
 console.log(stu2); // Teacher { name: 'james', age: 35 }

借用 Student 构造函数的函数体,创建 Teacher 的实例对象。(骚操作,class中super实现原理)

理解 receiver 参数

在上面 Proxy 的 set/get 捕捉器中,都接收一个参数 receiver。那么它有什么作用呢?

 const obj = {
   name: "copyer",
 };const objProxy = new Proxy(obj, {
   // receiver 形参
   get: function (target, key, receiver) {},
   // receiver 形参
   set: function (target, key, newValue, receiver) {},
 });

先从一个现象说起(可以自己动手试一下):

 const obj = {
   _name: "copyer",
   get name() {
     return this._name;
   },
   set name(newValue) {
     this._name = newValue;
   },
 };const objProxy = new Proxy(obj, {
   get: function (target, key) {
     console.log('get捕捉器')
     return Reflect.get(target, key);
   },
   set: function (target, key, newValue) {
     console.log('set捕捉器')
     Reflect.set(target, key, newValue);
   },
 });
 ​
 console.log(objProxy.name) // 只会打印一次 get捕捉器
 objProxy.name = '12' // 只会打印一次 set捕捉器

在定义对象的时候,使用了 get set 的方式,但是定义对象的本质是没变的,也就是说给 obj 定义了两个属性_name 和name。
想要的效果呢?就是无论是获取 name 还是 _name, 都应该被捕捉到(设置也是一样的)。
因为 get 和 set 的方式,当在获取还是在修改的时候,都是同时触发的。
简单来说,对 name 获取的时候,同时也在获取 _name;在设置name的时候,同时也在设置_name。
所以在捕捉对对象进行操作的时候,应该是打印两次 get捕捉器 或则 set捕捉器。而上面只打印了一次。
就说明,对 _name 进行获取和设置的时候,没有被捕捉到。具体的原因是 this 是指向的是 obj。this._name 等操作就没有经过代理对象,所以是捕捉不到的。
所以需要修改this的值?怎么做呢?
Proxy 的 get 和 set 提供了 receiver 参数,并且 Reflect.get 和 Reflect.set 也接受这个参数。
该参数就是去改变 this 的指向的,指向代理对象。

 const obj = {
   _name: "copyer",
   get name() {
     return this._name;
   },
   set name(newValue) {
     this._name = newValue;
   },
 };const objProxy = new Proxy(obj, {
   get: function (target, key, receiver) {
     console.log('get捕捉器')
     console.log(receiver === objProxy) // true 验证 receiver 就是代理对象
     return Reflect.get(target, key, receiver);
   },
   set: function (target, key, newValue, receiver) {
     console.log('set捕捉器')
     Reflect.set(target, key, newValue, receiver);
   },
 });

这样就正确了,可以捕捉到两次。

上次更新:
作者: ganfengchi