第十七节:Proxy代理对象和Reflect反射对象详解
一. Proxy详解
1. 抛砖引玉
需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程。
解决方案:可以使用 Object.defineProperty 的存储属性描述符(set、get)来对属性的操作进行监听。
弊端:Object.defineProperty设计的初衷,不是为了去监听截止一个对象中的所有属性的。(它的初衷是为了定义普通属性,并定义属性的一些特点,可写、可遍历等)
另外:如果我们想监听更加丰富的操作,比如新增、删除属性,Object.defineProperty是无能无力的。
{ console.log("---------------------1. 抛砖引玉---------------------"); const obj = { name: "ypf", age: 20, }; // 遍历监听每个属性 Object.keys(obj).forEach(key => { let myValue = obj[key]; Object.defineProperty(obj, key, { get() { console.log(`监听到obj对象的${key}属性被访问了`); return myValue; }, set(newValue) { console.log(`监听到obj对象的${key}属性被设置值`); myValue = newValue; }, }); }); obj.name = "ypf"; console.log(obj.age); }
2. Proxy含义
ES6中,新增了Proxy代理类,如果我们想监听一个对象的相关操作,就可以基于该对象创建一个Proxy代理对象,之后对该对象的所有操作(如:修改、获取等),都通过代理对象完成,并且代理对象可以监听到我们对原对象进行的那些操作。
3. Proxy的基本使用
(1). 如何创建代理对象?
标准格式: const p = new Proxy(target,handler); target表示需要被代理的对象,hander中用来配置一下捕获器,用来监听对象
比如:我要监听obj对象,则 const objProxy=new Proxy(obj,{});
注意:后续对象的操作,都需要通过代理对象来进行,否则无法触发监听
(2). 最常用的get和set捕获器
用来监听属性的读、写 操作
A. get(target, key, receiver) 参数含义依次为:被监听(代理)的对象、被获取属性的key、调用的代理对象(即obj)
B. set(target, key, newValue, receiver) 参数含义依次为:被监听(代理)的对象、被获取属性的key、调用的代理对象
{ console.log("--------2. 使用proxy监听对象的获取和设置操作----------"); const obj = { name: "ypf", age: 20, }; const objProxy = new Proxy(obj, { // 获取时的捕获器 get(target, key) { console.log(`监听到obj对象的${key}属性被访问了`); return target[key]; }, // 设置值时的捕获器 set(target, key, newValue) { console.log(`监听到obj对象的${key}属性被设置值`); target[key] = newValue; }, }); // 下面设置和获取属性都需要通过代理对象进行 objProxy, 否则无法触发监听 objProxy.name = "ypf1"; console.log(objProxy.age); }
4. Proxy其它捕获器
(1). has(): in 操作符的捕捉器
(2). deleteProperty(): delete 操作符的捕捉器
(3). apply(): 函数调用操作的捕捉器
(4). construct(): new 操作符的捕捉器
共有13个捕获器:
代码分享1:
// 3.1 has和defineProperty捕获器 { console.log("---------3.1 has和defineProperty捕获器-----------"); const obj = { name: "ypf", age: 20, }; const objProxy = new Proxy(obj, { // 监听in的捕获器 has(target, key) { console.log(`监听到obj对象的${key}属性的in操作`); return key in target; }, // 监听delete的捕获器 deleteProperty(target, key) { console.log(`监听到obj对象的${key}属性的delete操作`); delete target[key]; }, }); // 测试 console.log("name" in objProxy); delete objProxy.name; }
代码分享2:
// 3.2 apply和construct { console.log("---------3.2 apply和construct-----------"); function foo() {} const fooProxy = new Proxy(foo, { apply: function (target, thisArg, argArray) { console.log("对foo函数进行了apply调用"); return target.apply(thisArg, argArray); }, construct: function (target, argArray, newTarget) { console.log("对foo函数进行了new调用"); return new target(...argArray); }, }); fooProxy.apply({}, ["abc", "cba"]); new fooProxy("abc", "cba"); }
5. 捕获器receiver参数的作用
如下例子:改变了上面obj对象中this为ObjProxy,所以下面会调用两次 name一次,_name一次 (如果不传递receiver参数,只监听name,不监听_name)
// 4. 捕获器receiver参数的作用 { console.log("--------4. 捕获器receiver参数的作用----------"); const obj = { _name: "ypf", get name() { return this._name; }, set name(newValue) { this._name = newValue; }, }; const objProxy = new Proxy(obj, { get: function (target, key, receiver) { console.log(`监听到obj对象的${key}属性被访问了`); console.log(receiver === objProxy); //true receiver是创建出来的代理对象 //改变了上面obj对象中this为ObjProxy,所以下面会调用两次 name一次,_name一次 (如果不传递receiver参数,只监听name,不监听_name) return Reflect.get(target, key, receiver); }, set: function (target, key, newValue, receiver) { console.log(`监听到obj对象的${key}属性被设置值`); Reflect.set(target, key, newValue, receiver); }, }); // 测试 // objProxy.name = "lmr"; console.log(objProxy.name); }
6. 补充几个使用场景?
(1). 读操作的拦截--做非空判断
之前需要每个属性使用 || 或 ??,现在只需在代理对象的get捕获器中统一判断就行
{ console.log("-----5.1 读操作的拦截--做非空判断-----"); const obj = { name: "ypf", age: 17, }; //之前的模式 console.log(obj.height || ""); //返回空字符串 console.log(obj.height ?? ""); //返回空字符串 // 现在可以使用代理,就不用每个属性都去单独判空了 let objProxy = new Proxy(obj, { get(target, key) { return Reflect.has(target, key) ? target[key] : ""; }, }); // 调用 console.log(objProxy.height); //返回空字符串 }
(2). 写操作的拦截--服务端获取的数据希望是只读,不允许在任何一个环节被修改
之前遍历key,使用Object.defineProperty,设置writable: false,
现在只需要在代理的set捕获器中return false
{ console.log( "5.2 写操作的拦截--服务端获取的数据希望是只读,不允许在任何一个环节被修改" ); let jsonData = { status: "ok", msg: "获取成功", data: {}, }; // 之前的模式 Object.keys(jsonData).forEach(key => { Object.defineProperty(jsonData, key, { writable: false, }); }); // 使用代理 let jsonDataProxy = new Proxy(jsonData, { set(target, key, newValue) { return false; }, }); }
(3). 格式校验
之前的校验都写在业务中,现在可以使用proxy分离
{ // Validator.js export default (obj, key, value) => { if (Reflect.has(key) && value > 20) { obj[key] = value; } }; import Validator from "./Validator"; let data = new Proxy(response.data, { set: Validator, }); }
(4). 实例一个对象,每个对象都有一个自己的 id 而且只读
{ class Component { constructor() { this.proxy = new Proxy({ id: Math.random().toString(36).slice(-8), }); } get id() { return this.proxy.id; } } }
二. Reflect详解
1. 说明
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
(1).那么这个Reflect有什么用呢?
A. 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
B. 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
C. 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
(2). 如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
A. 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
B. 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
C. 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
D. 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
(3).那么Object和Reflect对象之间的API关系,可以参考MDN文档:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/Comparing_Reflect_and_Object_methods
2. Reflect常用方法
Reflect中有常见的方法和Proxy是一一对应的,也是13个。
最常见的用法,和Proxy同时使用: 因为Proxy是代理对象,既然已经代理,就不要使用原先对象了,比如在 get() set() 方法内部,可以使用Reflect的方法进行操作。
代码分享:
{ console.log("--------1. Reflect和Proxy同时使用---------"); const obj = { name: "ypf", age: 20, }; const objProxy = new Proxy(obj, { // 获取时的捕获器 get(target, key) { console.log(`监听到obj对象的${key}属性被访问了`); return Reflect.get(target, key); }, // 设置值时的捕获器 set(target, key, newValue) { console.log(`监听到obj对象的${key}属性被设置值`); Reflect.set(target, key, newValue); }, }); // 下面设置和获取属性都需要通过代理对象进行 objProxy, 否则无法触发监听 objProxy.name = "ypf1"; console.log(objProxy.age); }
3. Reflect.construct的作用
Reflect.construct(target, argumentsList[, newTarget]),对构造函数进行 new 操作,相当于执行 new target(...args)。
注意:如果传递newTarget,最后创建出来的对象是newTarget对象
代码分享:
{ console.log("---------------2. Reflect.construct的作用-------------------"); function Student(name, age) { this.name = name; this.age = age; } function Teacher() {} // const stu = new Student("why", 18) // console.log(stu) // console.log(stu.__proto__ === Student.prototype) // 执行Student函数中的内容, 但是创建出来对象是Teacher对象 const teacher = Reflect.construct(Student, ["ypf", 18], Teacher); console.log(teacher); console.log(teacher.__proto__ === Teacher.prototype); }
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。