Proxy代理和Reflect反射

63 阅读20分钟

新技术的出现,都是为了解决以前代码设计的不足之处。

ES6 中新增了两个API,Proxy 和 Reflect。那么这两个API有什么作用呢?应该怎么使用呢?

下面见分晓。

Proxy

为什么需要Proxy

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

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

回顾Object.defineProperty

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

属性描述符分为两种:

  1. 数据属性描述符(writable,value)
  2. 存取属性描述符(get,set)
js
复制代码
 // 数据属性描述符
 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/setwritable/value是不能共存的。

Object.defineProperty 监听

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

js
复制代码
 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 的方法可以实现监听,进行逻辑操作(在gettersetter中进行),那么这种方式有什么不好吗?

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

那么,Proxy就诞生了,出现的目的就是为了解决上面的一些列问题。

Proxy的基本使用

在 ES6 中新增了一个Proxy类,从名字就可以看出,帮助我们创建一个代理的。

  1. 简单的来说,监听对一个对象的操作,可以先创建一个代理对象(Proxy对象)。
  2. 之后对该对象的操作,都是通过对代理对象操作来完成的,代理对象可以监听了对原来对象进行了哪些的操作,进行捕捉

语法解析

new Proxy(target, handler)

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

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

Proxy的13种捕捉器

1、get 捕捉器(常用)

获取值的捕捉器

js
复制代码
 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 捕捉器(常用)

设置值的捕捉器

js
复制代码
 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操作符的捕捉

js
复制代码
 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 捕捉器(常用)

删除对象属性的拦截

javascript
复制代码
 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()作用:得到对象的原型。

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

6、setPrototypeOf 捕捉器

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

Object.setPrototypeOf()的作用:设置对象的原型指向另外一个对象

javascript
复制代码
 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(): 判断对象是否可以扩展。

js
复制代码
 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 方法的捕捉器。

禁止扩展,在新增上跟冻结是一样的效果。

js
复制代码
 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(): 获取属性描述符信息。

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

10、defineProperty 捕捉器

Object.defineProperty方法的捕捉器。

js
复制代码
 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值)

js
复制代码
 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调用)

js
复制代码
 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 操作符的捕捉器。

js
复制代码
 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即可,其他的翻阅资料就行。

Proxy 对象是在 ECMAScript 2015 (ES6) 中引入的。为 JavaScript 提供了一种强大的元编程机制,允许开发者拦截和自定义对象的基本操作。PS: Proxy代理的只能是对象, 原始值 string,number,boolean等不能被代理。

元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。通俗来说,元编程就是写代码来操作代码。举例: 通过在运行时动态地定义拦截规则,我们可以在对象的访问过程中插入自定义的逻辑。

基本语法

js
复制代码
const p = new Proxy(target, handler)

target

要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,Map,Set,WeakMap,WeakSet,原始值的包装类型,甚至另一个代理,但是不能是原始值 string,number,boolean,undefined,null,symbol[ES6],bigint[ES11])。

handler

处理程序对象, 包含一些列的拦截器(trap)

trap: 提供属性访问的方法。这类似于操作系统中捕获器的概念。例如,get,set 等

下面整理了一些主要的拦截器方法,它们定义在 handler 对象中:

  1. get(target, property, receiver): 拦截对象属性的读取操作。
  2. set(target, property, value, receiver): 拦截对象属性的设置操作。
  3. apply(target, thisArg, argumentsList): 拦截函数的调用操作。
  4. construct(target, argumentsList, newTarget): 拦截 new 操作符创建对象的操作。
  5. getPrototypeOf(target): 拦截对对象原型的访问。
  6. setPrototypeOf(target, proto): 拦截设置对象原型的操作。
  7. has(target, property): 拦截 in 操作符的操作。
  8. deleteProperty(target, property): 拦截对象属性的删除操作。

下面我们通过 日志记录和性能监控数据验证和保护动态代理和包装数据绑定和响应性延迟加载和懒执行权限控制 六个场景深入理解一下 Proxy 的实际应用。

Proxy 自身方法

除了拦截器方法之外,Proxy 对象本身还有一些方法。

Proxy.revocable 用于创建可撤销的代理,而 Proxy.ownKeys 用于获取代理对象的自有属性键。

Proxy.revocable(target, handler): 创建一个可撤销的代理对象。

安全性控制: 当需要在某个时刻撤销代理对象的拦截行为时,可使用 Proxy.revocable。这对于一些安全性控制的场景非常有用,例如限制对象的访问权限,而后根据需要撤销这些限制。

js
复制代码
const { proxy, revoke } = Proxy.revocable(target, handler);

// 撤销代理,取消拦截行为
revoke();

Proxy.ownKeys(target): 获取对象所有自有属性的键。

自有属性操作: 当需要获取代理对象的所有自有属性的键时,可以使用 Proxy.ownKeys。这对于一些场景,比如序列化对象、迭代对象属性等,是非常有用的。

js
复制代码
const keys = Proxy.ownKeys(proxy);

应用场景

场景1: 日志记录和性能监控

通过拦截对象的 getsetapply 等操作,记录对象的访问、修改、函数调用等操作,以实现日志记录和性能监控。

举例: 在每次访问对象属性或调用函数时,记录相应的日志信息。

js
复制代码
function createLoggingProxy(target) {
  return new Proxy(target, {
    get(target, property, receiver) {
      console.log(`访问属性 '${property}'`);
      // 对象方法
      if (typeof target[property] === 'function') {
        console.log(`调用函数 ${property}()`);
        const startTime = performance.now();
        const result = Reflect.get(target, property, receiver);
        const endTime = performance.now();
        console.log(`函数 ${property}() 执行时间:${endTime - startTime} 毫秒`);
        return result;
      }
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      console.log(`设置属性 '${property}' 为 ${value}`);
      return Reflect.set(target, property, value, receiver);
    },
    //apply(target, thisArg, argumentsList) {
    //  console.log(`调用函数 ${target.name}()`);
    //  const startTime = performance.now();
    //  const result = Reflect.apply(target, thisArg, argumentsList);
    //  const endTime = performance.now();
    //  console.log(`函数 ${target.name}() 执行时间:${endTime - startTime} 毫秒`);
    //  return result;
    //},
  });
}

// 创建一个普通对象
const myObject = {
  name: 'John',
  age: 25,
  greet() {
    console.log(`你好,我叫${this.name}`);
  },
  introduce() {
    console.log(`Hello, I am ${this.name}, and I am ${this.age} years old.`);
  },
};

// 创建日志记录代理对象
const loggingProxy = createLoggingProxy(myObject);

// 访问代理对象的属性,触发 get 拦截器
console.log(loggingProxy.name); // 输出: 访问属性 'name'

// 修改代理对象的属性,触发 set 拦截器
loggingProxy.age = 30; // 输出: 设置属性 'age' 为 30

// 调用代理对象的方法,触发 apply 拦截器
loggingProxy.greet(); // 输出: 调用函数 greet(), 函数 greet() 执行时间:(执行时间)
loggingProxy.introduce(); // 输出: 调用函数 introduce(), 函数 introduce() 执行时间:(执行时间)

场景2: 数据验证和保护

使用 set 拦截器来验证和保护对象属性的值,防止非法数据的设置。

示例: 对象属性值必须符合特定的数据类型或范围,否则拦截并抛出异常。

js
复制代码
function createValidationProxy(target) {
  return new Proxy(target, {
    set(target, property, value, receiver) {
      // 假设年龄必须是正整数
      if (property === 'age' && (!Number.isInteger(value) || value <= 0)) {
        throw new Error(`属性 'age' 的值必须是正整数,当前值为: ${value}`);
      }

      // 假设名字必须是非空字符串
      if (property === 'name' && (typeof value !== 'string' || value.trim() === '')) {
        throw new Error(`属性 'name' 的值必须是非空字符串`);
      }

      // 允许设置其他属性值
      return Reflect.set(target, property, value, receiver);
    }
  });
}

// 创建一个普通对象
const user = {
  name: 'John',
  age: 25
};

// 创建验证代理对象
const validationProxy = createValidationProxy(user);

// 设置属性,触发 set 拦截器
try {
  validationProxy.name = 'Alice'; // 正确的设置
  console.log(validationProxy.name); // 输出: Alice

  validationProxy.age = 30; // 正确的设置
  console.log(validationProxy.age); // 输出: 30

  validationProxy.age = -5; // 错误的设置,触发异常
} catch (error) {
  console.error(error.message); // 输出: 属性 'age' 的值必须是正整数,当前值为: -5
}

// 注意:这里的异常信息会显示在控制台

createValidationProxy 函数创建了一个代理对象,通过 set 拦截器验证和保护了属性值的设置。如果属性值不符合预定的条件(例如,年龄必须是正整数),则会抛出异常。在开发中,可以确保对象的属性值满足特定的数据类型或范围要求。

场景3: 数据绑定和响应性

使用 getset 拦截器,实现数据的双向绑定,当数据变化时自动更新相关视图。

示例: Vue.js 中的数据响应性系统就使用了类似的原理。

js
复制代码
function createReactiveObject(data, updateCallback) {
  return new Proxy(data, {
    get(target, property, receiver) {
      // 在属性访问时执行操作
      console.log(`访问属性:${property}`);
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      // 在属性设置时执行操作
      console.log(`设置属性:${property},新值:${value}`);

      // 设置属性值
      const result = Reflect.set(target, property, value, receiver);

      // 触发更新回调
      updateCallback(property, value);

      return result;
    }
  });
}

// 模拟一个视图更新的回调函数
function updateView(property, value) {
  console.log(`视图更新:${property} -> ${value}`);
}

// 创建一个普通对象
const user = {
  name: 'John',
  age: 25
};

// 创建响应式对象
const reactiveUser = createReactiveObject(user, updateView);

// 访问属性,触发 get 拦截器
console.log(reactiveUser.name); // 输出: 访问属性:name, John

// 修改属性,触发 set 拦截器,并触发视图更新
reactiveUser.age = 30; // 输出: 设置属性:age,新值:30, 视图更新:age -> 30

代理对象使用 getset 拦截器,监听属性的访问和设置操作,并在属性设置时触发更新回调,实现了简单的数据双向绑定。这里的 updateView 函数模拟了视图更新的回调,实现了一个简单的数据双向绑定。

场景4: 延迟加载和懒执行

使用 get 拦截器,实现对属性的延迟加载或懒执行,只在需要时才进行实际的计算或加载。

示例: 懒加载图片,只有在图片被访问时才进行加载。

js
复制代码
// 模拟一个异步加载图片的函数
function loadImage(url) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.onload = () => resolve(image);
    image.onerror = reject;
    image.src = url;
  });
}

// 创建一个包含图片 URL 的普通对象
const imageUrls = {
  cat: 'https://example.com/cat.jpg',
  dog: 'https://example.com/dog.jpg'
};

// 创建懒加载图片对象
const lazyLoadImages = new Proxy(imageUrls, {
  get(target, property, receiver) {
    if (property in target) {
      // 属性已存在,直接返回 Promise,表示图片加载过程
      console.log(`访问已存在图片属性:${property}`);
      return loadImage(target[property]);
    } else {
      // 属性不存在,返回一个 resolved 的 Promise,表示空白图片
      console.log(`访问不存在图片属性:${property},懒加载图片`);
      return Promise.resolve(new Image());
    }
  }
});

// 访问已存在属性,触发图片加载
lazyLoadImages.cat.then((image) => {
  console.log('图片加载成功:', image);
}).catch((error) => {
  console.error('图片加载失败:', error);
});

// 访问不存在属性,返回 resolved 的 Promise,表示空白图片
lazyLoadImages.dog.then((image) => {
  console.log('懒加载图片成功:', image);
}).catch((error) => {
  console.error('懒加载图片失败:', error);
});

当访问图片属性时,如果属性已存在,返回一个 Promise 表示图片加载过程;如果属性不存在,返回一个 resolved 的 Promise 表示空白图片。在实际的应用中,可以根据需要结合DOM进一步优化加载逻辑。

场景5: 权限控制

使用 getset 拦截器,对对象属性的访问和修改进行权限控制。

示例: 针对用户角色,限制对对象某些属性的读写权限。

js
复制代码
// 用户角色
const userRoles = {
  admin: 'admin',
  regularUser: 'regularUser'
};

// 创建一个普通对象
const securedObject = {
  name: 'John',
  age: 25
};

// 创建权限控制代理对象
function createPermissionControlProxy(target, userRole) {
  return new Proxy(target, {
    get(target, property, receiver) {
      // 检查用户角色是否有读取属性的权限
      if (userRole === userRoles.admin || property !== 'age') {
        // 有权限或不是敏感属性,返回属性值
        console.log(`访问属性:${property}`);
        return Reflect.get(target, property, receiver);
      } else {
        // 没有权限,拒绝访问
        console.log(`无权限访问属性:${property}`);
        throw new Error(`无权限访问属性:${property}`);
      }
    },
    set(target, property, value, receiver) {
      // 检查用户角色是否有修改属性的权限
      if (userRole === userRoles.admin || property !== 'age') {
        // 有权限或不是敏感属性,设置属性值
        console.log(`设置属性:${property},新值:${value}`);
        return Reflect.set(target, property, value, receiver);
      } else {
        // 没有权限,拒绝修改
        console.log(`无权限设置属性:${property}`);
        throw new Error(`无权限设置属性:${property}`);
      }
    }
  });
}

// 模拟一个管理员用户
const adminUser = userRoles.admin;

// 创建管理员权限控制代理对象
const adminProxy = createPermissionControlProxy(securedObject, adminUser);

// 访问属性,有权限
console.log(adminProxy.name); // 输出: 访问属性:name, John

// 修改属性,有权限
adminProxy.age = 30; // 输出: 设置属性:age,新值:30

// 创建一个普通用户
const regularUser = userRoles.regularUser;

// 创建普通用户权限控制代理对象
const regularUserProxy = createPermissionControlProxy(securedObject, regularUser);

// 访问敏感属性,无权限,抛出异常
try {
  console.log(regularUserProxy.age);
} catch (error) {
  console.error(error.message); // 输出: 无权限访问属性:age
}

// 修改敏感属性,无权限,抛出异常
try {
  regularUserProxy.age = 28;
} catch (error) {
  console.error(error.message); // 输出: 无权限设置属性:age
}

代理对象使用 getset 拦截器,根据用户角色限制对敏感属性的读写权限。管理员用户拥有全部权限,普通用户只能读写非敏感属性。

注意事项和细节

  1. 目标对象的可扩展性: 如果目标对象是不可扩展的(Object.preventExtensions()Object.seal()Object.freeze()),那么在代理对象上添加新属性会导致错误。

    js
    复制代码
    const target = Object.freeze({ name: 'John' });
    const handler = {};
    const proxy = new Proxy(target, handler);
    
    // 会抛出错误
    proxy.age = 30;
    
  2. 不可撤销的代理: 一旦创建了代理对象,就无法将其还原回原始对象。如果你尝试撤销代理,会得到一个错误。

    js
    复制代码
    const target = { name: 'John' };
    const handler = {};
    const proxy = new Proxy(target, handler);
    
    // 撤销代理会抛出错误
    Proxy.revocable(proxy, handler).revoke();
    
  3. Reflect 方法的正确使用: 在拦截器中使用 Reflect 方法是一个良好的实践,以确保代理的正确行为

    js
    复制代码
    const handler = {
      get(target, property, receiver) {
        // 推荐使用 Reflect.get
        return Reflect.get(target, property, receiver);
      }
    };
    
  4. 循环引用: 避免创建循环引用,因为代理对象可能导致无限递归,从而导致堆栈溢出。

    js
    复制代码
    const target = {};
    const handler = {
      get(target, property, receiver) {
        return target[property];
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    // 创建循环引用,潜在的堆栈溢出
    target.circularRef = proxy;
    
  5. 不要滥用代理: 使用代理时应该明确其必要性,不要过度使用。过多的代理可能导致代码难以理解和维护。

  6. 性能考虑: Proxy 的使用可能引入一些性能开销,特别是在频繁调用的场景。在性能敏感的情况下,应该仔细评估代理的影响。

  7. 拦截器的顺序: 如果使用了多个拦截器,它们的执行顺序是重要的。确保了解拦截器执行的顺序,以便预测代理的行为。

  8. 非整数属性的顺序: 如果代理对象有非整数属性,那么 for...inObject.keys() 的顺序可能是不确定的。最好避免依赖于属性的顺序。


使用 Proxy 时需要谨慎,考虑代理的目的、代理对象的特性以及可能的影响。了解这些细节可以帮助你更好地使用 Proxy,避免潜在的问题。

其它问题

diff
复制代码
PS: proxy 代理对象 和 被代理的对象 target 的关系
- 代理对象通过 Proxy 构造函数创建,需要传入目标对象 target 和拦截器对象 handler。
- 代理对象通过拦截器方法对目标对象的操作进行拦截、处理和定制。
- 操作代理对象实际上是通过拦截器方法委托给目标对象执行相应的操作。
- 直接操作目标对象会影响代理对象
lua
复制代码
PS:代理的对象有的方法或属性, 我在handler都有都能拦截吗?

基本上是正确的,但有一点需要注意。Proxy 的 handler 对象中可以包含多个拦截器方法,其中大多数方法对应于被代理对象的方法或属性,可以进行拦截。例如,get 拦截器用于拦截对象属性的读取,set 拦截器用于拦截对象属性的设置,等等。

然而,不是所有的对象方法都可以被拦截。一些特殊的对象方法,例如 Object.preventExtensions、Object.seal、Object.freeze 等,以及 Symbol.* 等方法,并不能被 Proxy 的拦截器所捕获。

另外,一些内部方法,比如 [[GetPrototypeOf]][[SetPrototypeOf]][[IsExtensible]][[PreventExtensions]] 等,也不能被 Proxy 直接拦截。

因此,在使用 Proxy 时,需要根据具体的需求选择合适的拦截器方法,了解哪些方法可以被拦截,哪些方法不能被拦截。在 MDN 等文档中,可以找到关于每个拦截器方法的详细信息,以更好地理解 Proxy 的使用和限制。

Reflect

Reflect 是 ES6 新增的一个API,它是一个对象,字面的意思是反射

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

知道两点即可:

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

Reflect的作用

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

比如:

  1. Reflect.getPrototypeOf() 与 Object.getPrototypeOf() 类似
  2. Reflect.defineProperty() 与 Object.defineProperty() 类似
  3. ...

到了这里,是不是有疑问?

既然有了 Object 提供了这些方法,为什么还需要 Reflect 的呢?

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

基于上面的问题,Reflect 就是为了解决上面的一些列问题,是API设计看起来更加的规范。

MDN:对比Reflect与Object

看了MDN的对比之后,不能发现,有些方法 Object 含有,Reflect 没有;有些方法 Reflect 含有,但是 Object 没有。

所以说,Reflect 并不能完全替代 Object,只是在某一些方面是可以转变的。

Proxy 和 Reflect 的配合使用

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

js
复制代码
 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种捕捉器

js
复制代码
 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种捕捉器

js
复制代码
 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 调用函数的操作。

具体语法

js
复制代码
 Reflect.construct(target, argumentsList[, newTarget])
 // 相当于 new Target(...argumentsList)

用法一:创建实例对象

js
复制代码
 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 }

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

ini
复制代码
 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。那么它有什么作用呢?

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

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

javascript
复制代码
 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 定义了两个属性_namename

想要的效果呢?就是无论是获取 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 的指向的,指向代理对象。

js
复制代码
 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);
   },
 });

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