Proxy和Reflect的理解

前沿

这篇《从 Proxy 到 Vue 源码,深入理解 Vue 3.0 响应系统》提到了vue中通过Object.defineProperty 的,递归遍历 data 对象上的所有属性,将其转换为 getter/sette

 

Proxy和Reflect的理解

如果用proxy代理对象来替代target对象的话,如果不想对外暴露target对象,该对象不要申明,而是以对象字面量的形式写在new Proxy()中。

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

接收两个参数:被代理者和处理代理的对象。

Proxy称为代理,当通过代理来操作被代理者时,会触发代理相关的代码;如果直接操作被代理者,不会触发代理相关代码。

const handler = {
  get(target, prop, receiver){
    console.log(target); 被代理者obj对象 {name: "zhu", age: 24}
    console.log(prop);   被代理者的属性名 name
    console.log(receiver);  Proxy或者继承Proxy的对象   Proxy {name: "zhu", age: 24}
    return "proxy调用了该方法"
  }
}
let obj = {
  name: "zhu",
  age: 24
}
let proxy = new Proxy(obj, handler);
console.log(obj.name);  打印zhu,无法触发代理代码
console.log(proxy.name); 打印 proxy调用了该方法,触发代理代码中赋值相关的代码
console.log(proxy);       Proxy {name: "zhu", age: 24}

Reflect:需要在Proxy内部调用对象的默认行为,即执行Proxy时,将方法转到目标对象上,让目标对象也生效。

Reflect是内置静态对象,不是构造函数,不能进行new的操作,Reflect对象上有和Proxy对应的静态方法,可直接使用。就行Math对象上的方法一样。

基本特点:引用自:https://zhuanlan.zhihu.com/p/60126477

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在。这些方法能够执行默认行为,无论Proxy怎么修改默认行为,总是可以通过Reflect对应的方法获取默认行为。
  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
  • Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

 比如上面的get方法,我们通过Reflect反射到目标对象上,获得目标对象的对应属性的值:

const handler = {
  get(target, prop, receiver){  
    return Reflect.get(target,prop);
  }
}
let obj = {
  name: "zhu",
  age: 24
}
let proxy = new Proxy(obj, handler);
console.log(proxy.name);   zhu

也可以单独调用Reflect方法,对对象进行操作设置,比如:

var obj = {};
Reflect.set(obj, "name", "zhu"); // true
obj.name; // "zhu"
// Array
var arr = ["duck", "duck", "duck"];
Reflect.set(arr, 2, "goose"); // true
arr[2]; // "goose"

// It can truncate an array. 截断数组
Reflect.set(arr, "length", 1); // true
arr; // ["duck"];

1、Proxy对象的get方法和Reflect.get()

(1)Proxy对象的get方法 用于代理处理对象读取属性的操作。

以下是传递给get方法的参数,this上下文绑定在handler对象上.

  • target 目标对象。
  • property 被获取的属性名。
  • receiver   Proxy或者继承Proxy的对象。

返回值:get方法可以返回任何值。

该方法会处理目标对象的以下操作:

  • 访问属性: proxy[foo]和 proxy.bar
  • 访问原型链上的属性: Object.create(proxy)[foo]
  • Reflect.get()
const handler = {
  get(target, prop, receiver){
    console.log(target);    被代理者obj对象 {name: "zhu", age: 24}
    console.log(prop);      被代理者的属性名 name
    console.log(receiver);  Proxy或者继承Proxy的对象   Proxy {name: "zhu", age: 24}
    return Reflect.get(target,prop);
  }
}
let obj = {
  name: "zhu",
  age: 24
}
let proxy = new Proxy(obj, handler);
console.log(proxy.name);   get方法的返回值:zhu

如果违背了以下的约束,proxy会抛出 TypeError:

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
  • 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined。

关于第一条的异常:只有当configurable和writable同时为false的时候,get方法的返回值不能修改该属性的值,不然会报错。

var obj = {};
Object.defineProperty(obj, "a", { 
  configurable: false, 
  enumerable: false, 
  value: 10, 
  writable: false 
});

var p = new Proxy(obj, {
  get: function(target, prop) {
    return 20;
  }
});
console.log(p.a);    会抛出TypeError

(2) Reflect.get()方法与从 对象 (target[propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。

  • target  需要取值的目标对象
  • propertyKey 需要获取的值的键值
  • receiver   如果target对象中指定了getterreceiver则为getter调用时的this值。

返回属性的值

2、Proxy对象的set

handler.set() 方法用于代理处理对象设置属性值的操作。

以下是传递给 set() 方法的参数。this 绑定在 handler 对象上。

  • target 目标对象。
  • property 将被设置的属性名或 Symbol
  • value 新属性值。
  • receiver  最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。
const handler = {
  set(target, prop, value, receiver) {
    console.log(this);   handler对象
    console.log(target); 目标对象   {name: "zhu", age: 24}
    console.log(prop);   将被设置的属性名或 Symbol  name
    console.log(value);  设置的属性值   zhuyuzhu
    console.log(receiver); 最初被调用的对象  Proxy {name: "zhu", age: 24}
  }
}
let obj = {
  name: "zhu",
  age: 24
}
let proxy = new Proxy(obj, handler);
proxy.name = "zhuyuzhu";

比如:假设有一段代码执行 obj.name = "jen"obj 不是一个 proxy,且自身不含 name 属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的 set() 处理器会被调用,而此时,obj 会作为 receiver 参数传进来。

let proxyObj = new Proxy({name:"fa"},{
  set(target, prop, value, receiver){
    console.log(receiver);   谁调用的该方法,receiver就是谁。此处是proxyObj的子对象obj
    console.log(target);   target指向Proxy传入的第一个参数,目标对象
    return Reflect.set(target,prop,value);
  }
})
let obj  = Object.create(proxyObj);
obj.name = "child";

(2)Reflect.set()方法

  • target  设置属性的目标对象。
  • propertyKey  设置的属性的名称。
  • value  设置的值。
  • receiver  如果遇到 setterreceiver则为setter调用时的this值。

3、Proxy对象和Reflect对象其他的方法

(1)handler.apply() 方法用于拦截函数的调用。

以下是传递给apply方法的参数,this上下文绑定在handler对象上.

  • target 目标对象(函数)。
  • thisArg 被调用时的上下文对象。
  • argumentsList 被调用时的参数数组

apply方法可以返回任何值。但是target必须是一个函数。

该方法会拦截目标对象的以下操作:

  • proxy(...args)
  • Function.prototype.apply() 和 Function.prototype.call()
  • Reflect.apply()

实例:注意也可以操作target

function sum(a, b) {
  return a + b;
}
const handler = {
  //target目标函数,thisArg执行上下文,argumentsList参数列表数组
  apply: function(target, thisArg, argumentsList) {
    console.log(`Calculate sum: ${argumentsList}`);
    // return target(argumentsList[0], argumentsList[1]) * 10;
    return Reflect.apply(target,thisArg,argumentsList) * 10
  }
};

const proxy1 = new Proxy(sum, handler);

console.log(sum(1, 2));
// expected output: 3
console.log(proxy1(1, 2));
// expected output: 30

(2)handler.has() 方法是针对 in 操作符的代理方法。

下面是传递给 has 方法的参数. this is bound to the handler.

  • target 目标对象. 
  • prop 需要检查是否存在的属性.

 has 方法返回一个 boolean 属性的值.

handler.has 可以拦截下面这些操作:

  • 属性查询: foo in proxy
  • 继承属性查询: foo in Object.create(proxy)
  • with 检查: with(proxy) { (foo); }
  • Reflect.has()

注意:在方法中最后也是可以返回操作的target的,但是用Reflect反射到目标对象上会更好

const handler1 = {
  has(target, key) {
      // return key in target;
      return Reflect.has(target,key);
    }
};
const monster1 = {
  _secret: 'easily scared',
  eyeCount: 4
};
const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1);
// expected output: true
console.log('_secret' in proxy1);
// expected output: false
console.log('_secret' in monster1);
// expected output: true

 (3)handler.getPrototypeOf() 是一个代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用。

以下这几个操作都会触发代理的getPrototypeOf方法:

  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • __proto__
  • Object.prototype.isPrototypeOf()
  • instanceof

(4)handler.setPrototypeOf() 方法主要用来拦截Object.setPrototypeOf()和Reflect.setPrototypeOf()的操作。

(5)handler.ownKeys() 方法用于拦截以下这个几种操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • Reflect.ownKeys()

Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。(对象自身的所有属性,包括不可遍历属性和Symbol属性)它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。

(6)handler.defineProperty() 用于拦截对对象的Object.defineProperty()和eflect.defineProperty()操作

(7)handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。包括:删除属性: delete proxy[foo] 和 delete proxy.foo和Reflect.deleteProperty()

(8)handler.getOwnPropertyDescriptor() 方法是 Object.getOwnPropertyDescriptor()  的钩子。

陷阱可以拦截这些操作:Object.getOwnPropertyDescriptor()和Reflect.getOwnPropertyDescriptor()

(9)handler.isExtensible() 方法用于拦截对对象的Object.isExtensible()。

(10)handler.preventExtensions() 方法用于设置对Object.preventExtensions()的拦截

总结:

(1)代理的每个方法除了拦截之前了解的一些操作外,肯定会拦截Reflect对象对应的方法。

(2)对对象属性的获取、设置、删除,遍历,判断对象是否有某个属性的方法:get方法、set方法、deleteProperty方法、ownKeys方法、has方法。

(3)对象获取原型,设置原型,配置对象的属性,获取对象属性的描述信息,判断是否可拓展,阻止对象拓展

getPrototypeOf方法,setPrototypeOf方法、defineProperty方法,getOwnPropertyDescriptor方法,isExtensible方法,preventExtensions方法

(4)函数的调用和拦截new 操作符:apply方法和handler.construct()

(5)Proxy.revocable()方法:该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke}

proxy:表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉。

revoke:撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。

4、实例

(1)校验数据

let numberObj = {};
let proxyNumObj = new Proxy(numberObj, {
  set(target, prop, value) {
    if(typeof value !=="number") {
      throw Error("numberObj的属性值必须是数字"); //错误类型
    }
    return Reflect.set(target, prop, value);
  }
})
proxyNumObj.age = 24;
proxyNumObj.name = "zhu";

 (2)实现对象私有方法和私有属性

只有该对象内部可以调用,禁止外部调用该对象的私有属性和方法。在 JavaScript 或其他语言中,大家会约定俗成地在变量名之前添加下划线 _ 来表明这是一个私有属性(并不是真正的私有),但我们无法保证真的没人会去访问或修改它。在下面的代码中,我们声明了一个私有的属性和方法,便于这个对象内部的方法调用,但不希望从外部也能够访问到他们。

该实例中使用到可销毁的Proxy对象:

let obj = {
  name: "zhu",
  age: 24,
  func1: function(){
    let that = obj;//防止修改this指向
    //内部访问的时候,关闭再开启Proxy---感觉有点麻烦,但是没有想到好的解决办法
    proxyObj.revoke();
    console.log("可以通过func1访问到_love属性和_func2方法");
    that._func2();
    console.log(that._love);
    proxyObj = Proxy.revocable(that, handler);//开启
    proxy = proxyObj.proxy;
  },
  _func2(){
    console.log("_func2方法在内部执行了");
  },
  _love: "null"
};
let handler =  {
  get(target, prop, receiver){
    if(prop === "_love" || prop === "_func2"){ //是对象中带有_私有属性,允许其他开发者在该对象上添加_这样的属性
      if(typeof prop === "string"){
        return undefined;
      }else {
        throw Error(`${prop} is not function`)
      }
      
    }
    return Reflect.get(target,prop,receiver);
  },
  set(target, prop, value, receiver){

  }
}
let proxyObj = Proxy.revocable(obj, handler); //proxyObj才可以调用revoke方法
let proxy = proxyObj.proxy; //proxy是Proxy对象,对外proxy就是obj

proxy.func1();
console.log(proxy._love);  //undefined
console.log(proxy.name);  //zhu
proxy._func2(); //报错!

 创建可销毁的Proxy对象

创建proxy对象的父对象:

let proxyObj = Proxy.revocable(obj, handler);

该对象有一个属性proxy对象和一个销毁方法revoke

{proxy: Proxy, revoke: ƒ}

可以用解构赋值进行赋值:

let {proxy, revoke} = Proxy.revocable(obj, {});

 销毁可执行revoke方法,那么父对象内的proxy对象就是空:

proxyObj.revoke();

父对象内,空的proxy对象:

Proxy和Reflect的理解_第1张图片

 重新开启,再重新赋值即可:

    proxyObj = Proxy.revocable(that, handler);//开启
    proxy = proxyObj.proxy;

(3)单例模式

保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。

handler.construct() 方法用于拦截new 操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的,说明目标对象是一个函数)。

下面的参数将会传递给construct方法,this绑定在handler上。

  • target  目标对象。
  • argumentsList  constructor的参数列表。
  • newTarget  最初被调用的构造函数,就上面的例子而言是p。

该拦截器可以拦截以下操作:

  • new proxy(...args)
  • Reflect.construct()

必须返回一个对象,不然代理将会抛出错误 TypeError。

实例:创建一个函数的Proxy对象,通过构造函数创建

注意target就代表了目标函数Monster,所以return的函数可以通过new target的形式得到一个对象;也可以通过Reflect.construct的形式,反射到目标对象上。

function Monster(disposition) {  Monster巨人  disposition性情
  this.disposition = disposition;
}
const handler1 = {
  construct(target, args) {  拦截构造函数或class类的new操作,target就是构造函数或class类
    return new target(...args) || Reflect.construct(target, args);
  }
};
const proxy1 = new Proxy(Monster, handler1);
console.log(new proxy1('fierce').disposition); fierce凶猛的

实例:单例模式,通过类创建

保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。在修改创建的单例对象时,也需要修改对应的存储它的变量flag。

class Monsters  {
  constructor(disposition, name){
    this.disposition = disposition;
    this.name = name;
  }
}
let handler = {
  flag: null,
  construct(target, args, newTarget){
    // 
    if(!this.flag){
      let obj = Reflect.construct(target, args) || new target(...args);
      this.flag = obj;
      return obj;
    }
    throw Error(`only can construct one Object!`);
  }
}
let proxy = new Proxy(Monsters, handler)
let monster1 = new proxy("fierce", "lizhi");
monster1 = null; handler.flag = null;
let monster2 = new proxy("aa","zhu");
console.log(monster2);

实例:实现对象多继承(重点实例)

该实例转自《深入实践 ES6 Proxy & Reflect》

const people = {
  name: 'people',
  run() {
    console.log('people.run:', this.name);
  }
};

const powerMan = {
  name: 'powerMan',
  run() {
    console.log('powerMan.run:', this.name);
  },
  fight() {
    console.log('powerMan.fight:', this.name);
  }
};

const handler = {
  get(target, prop, receiver) {
    if (Reflect.has(target, prop)) {//实际上是for in。此处也巧妙应用了Reflect对象。
      return Reflect.get(target, prop, receiver);
    }
    else {
      for (let P of target[Symbol.for('[[Prototype]]')]) {
        if (Reflect.has(P, prop)) {
          return Reflect.get(P, prop, receiver);
        }
      }
    }
  }
};
//对象字面量,不对外暴露目标对象
Object.prototype.a = 1;
const hero = new Proxy({
  name: 'hero',
  strike() {
    this.run();
    this.fight();
  }
}, handler);
//在全局Symbol注册表上注册一个属性Prototype,该属性存储两个
hero[Symbol.for('[[Prototype]]')] = [people, powerMan];
hero.strike();
console.log(Reflect.has({},"a")); 

上面代码解读:

(1)Reflect.has({},"a")判断对象及其原型上是否有“a”属性,对Reflect的理解

(2)Reflect的方法不仅仅是非要用在Proxy代理对应的方法上,而是可以灵活的使用。

(3)Symbol.for全局注册一个值为[[Prototype]]的Symbol值。该Symbol值可以作为对象的属性去使用,注意:是将该Symbol值作为对象的属性,而用在每个对象上时,作为属性,它的值是要每个对象去设置的。

代码执行的流程:

1)当执行hero.strike()时,要去hero代理上看有没有strike方法(get方法得到执行);

2)然后执行run方法和fight方法时,同样要去hero代理上去看对象及原型上有没有该方法,没有这两个方法,去hero的Symbol.for('[[Prototype]]')属性上看有没有这两个方法,有才会执行,

3)这个两个方法执行的时候,this是hero,因为hero是执行上下文this。

 

你可能感兴趣的:(ES6,proxy)