Proxy对象

279 阅读5分钟

一、Object.defineProperty(obj, prop, descriptor)

  1. 该方法会【直接】在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol(被劫持的属性)。
  • descriptor:要定义或修改的属性描述符。

juejin.cn/post/691410…

  1. Object.defineProperty在劫持对象和数组时的缺陷:
  • 无法检测到对象属性的添加或删除
  • 无法检测数组元素的变化,需要进行数组方法的重写
  • 无法检测数组的长度的修改
let user = {
      age: 16,
      name: "啊橙",
    }
    /*
      如果直接在get函数中return user.name的话,这里的user.name同时也会
      调用一次get函数,这样的话会陷入一个死循环;
      set函数也是同样的道理,因此我们通过一个第三方的变量initName来防止死循环。
    */
    var initName = '啊橙'

    // 通过defineProperty定义的属性会在user上定义一个name属性,并为其绑定setter和getter
    Object.defineProperty(user, 'name', {
      get() {
        console.log('触发--> get')
        return initName
      },
      set(val) {
        console.log('触发--> set')
        initName = val
      }
    })


   // user.name  // 触发get

二、Porxy

2.1 Porxy简介

  1. Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写

  2. 相较于Object.defineProperty劫持某个属性,Proxy直接代理了target整个对象,并且【返回了一个新的对象】,通过监听代理对象上属性的变化来获取目标对象属性的变化;Proxy还能监听到属性的增加和删除,比Object.defineProperty的功能更为强大。

  3. 不管是数组下标或者数组长度的变化,还是通过函数调用,Proxy都能很好的监听到变化;而且除了我们常用的get、set,Proxy更是支持13种拦截操作。

  4. Proxy是一个构造函数,通过new Proxy生成拦截的实例对象,让外界进行访问;

构造函数中的:
    target:所要拦截的目标对象,可以是对象或者数组;
    handler:用来定制拦截规则/配置对象,只能是对象,和Object.defineProperty中的descriptor描述符有些类似
    var proxy = new Proxy(target, handler)
    
    handler = {
      // 目标(拦截)对象、拦截对象的属性名、proxy实例本身      其中最后一个参数可选
      get:function(target, key, receiver){
      
      },
      // 目标(拦截)对象、拦截对象的属性名、属性值、Proxy实例本身    其中最后一个参数可选
      set:function(target, key, value, receiver){ 
          
      }
    }
        

Reflect: 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上

5.示例1

let target = { name: "啦啦啦啦" }   // 监听对象
// let target = [10,21,13,41]       // 监听数组
    
var proxy = new Proxy(target, {
  // 拦截对象 访问的拦截对象属性 指向proxy对象
  get(target, key, receiver) {  
    console.log(`触发get--> 属性为:${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`触发set -------> 属性为:${key}!`);
    // Reflect.set 如果不传入receiver 那么就不会触发defineProperty函数
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key) {
    console.log(`触发删除函数 删除的属性为:${key}!`);
    delete target[key];
    return true;
  },
  defineProperty(target, key, attribute) {
    console.log('触发defineProperty');
    Reflect.defineProperty(target, key, attribute);
  }
})


console.log(proxy)  
proxy的结构如下↓:

proxy:{
    Handler:{}   就是new Proxy时传递的配置对象或者叫代理规则
    Target:{}    就是new Proxy时传递的所要拦截的目标对象
}

// 取值
proxy.name;       // 触发get--> 属性为:name!   返回'啦啦啦啦'
// 赋值
proxy.count = 1;  // 触发set -------> 属性为:ads!    触发defineProperty  返回1
// 删除
delete proxy.count  // 触发删除函数 删除的属性为:count!   返回true

如果 Proxy对象和Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。

上面代码中,Proxy.set拦截里面使用了Reflect.set,而且传入了receiver,导致触发Proxy.defineProperty拦截。这是因为Proxy.set的receiver参数总是指向当前的Proxy实例,而Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。

6.示例2

let target_1 = {}  // 目标对象
let handler_1 = {}

// handler_1 是一个空对象,没有任何拦截效果,访问proxy就等同于访问target
let proxy_1 = new Proxy(target_1, handler_1)
proxy_1.a = 'b'  // 'b'
target_1.a      // "b"

2.2. Proxy 实例的方法 

2.2.1 get

// 例1:访问代理对象上不具有的属性就报错
let userInfo = {  name : 'wuwei' }

let userInfoProxy = new Proxy(userInfo,{
  get(target,property){   
    if(property in target){   // in 操作符用来判断某个属性属于某个对象
      console.log(`你访问了${property}属性`)
      return target[property];    // proxy可以这么写,但是Object.defineProperty 这么写会死循环
    }else{
      throw new ReferenceError(`${property}属性不在此对对象上`)
    }
  }
})
// 例2:下面的例子使用get拦截,实现数组读取负数的索引。
function createArray(...elements) {  // 将传递给函数的所有实参打包到一个数组中
  let target = []
  target.push(...elements)
  return new Proxy(target, {
    get(target, key, receiver) {
      if (Number(key) < 0) {
        key = String(target.length + Number(key))
      }
      return Reflect.get(target, key, receiver)
      // return target[key]  两种都可以
    }
  })
}
let arr = createArray('a', 'v', 'f')
console.log(arr);
// arr[-1]   // 'f'


// 例3:
// 利用Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。
// 人话:利用proxy的get属性,每次调用都返回proxy 每次调用proxy的get属性都会触发一个函数
let pipe = function (value) {
  let arr = [];  // 存放函数的
  let target = {}

  let oproxy =  new Proxy(target, {
    get(target, key ,receiver) {
      console.log(target,key,receiver)

      // 如果调用了get  转变为执行某个函数
      if (key === 'get') {
        return arr.reduce((prev, current)=>{   // current就是函数 prev就是要计算的数据
          console.log(prev,'*---*',current)
          return current(prev);
        }, value);
      }

      // 将函数push进arr数组中
      arr.push(window[key]);
      console.log(arr)
      // 触发get函数 不再返回对应的值 return target[key] 或者 return Reflect.get(target,key,receiver)
      return oproxy;  // 返回的是proxy
    }
  });

  console.log(999)
  return oproxy;
}

// 务必用var 变量提升 这个一个函数
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
var add = n => n


/*
  pipe函数默认返回Proxy的实例化对象
  当打点调用时(非调用get)就会触发get函数,把被调用的函数key保存到数组中,此时默认也返回一个Proxy的实例化对象,
  当最后打点调用get时,就会把数组中的函数执行一遍。最终返回结果

  最后遍历把arr的三个函数执行一遍
*/
// pipe(3).double.pow.get; // 36
pipe(3).double.pow.reverseInt.get; // 63


2.2.2 set

// 例1:假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,
// 那么可以使用Proxy保证age的属性值符合要求。
let target1 = {}
let proxy1 = new Proxy(target1,{
  set(target1, key, value, receiver){
    if(key === 'age'){
      // Number 对象中的 isInteger() 方法用来判断传入的参数是否为整型
      if(!Number.isInteger(value)){
        throw new TypeError('类型错误!')
      }
      if(value>200){
        throw new RangeError('范围错误 参数超范围');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    // Reflect.set(target1, key, value, receiver)
    target1[key] = value;
    return true;  // 严格模式下必须要返回一个true

  }
})

/*
  上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,
  这是数据验证的一种实现方法。
*/


// 例2:利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
let data = { count:1 }
let dataProxy = new Proxy(data,{
  set(data, key, value, receiver){
    data[key] = value
    document.querySelector('#h3').innerText = receiver.count
    console.log(data);
  }  
})
const handleInput = (event) => {
  let value = event.target.value
  dataProxy.count = value
}
    

2.2.3 其他方法

let obj = {
    a:100,
    b:200
}

let objProxy = new Proxy(obj, {
  deleteProperty(target, key) {
    console.log('delete' + ' -> ' + key);
    delete target[key];
    return true; 

    // return Reflect.deleteProperty(target, name);  // proxy配合Reflect使用
  },
  has(target, key) {
    console.log('has' + ' -> ' + key);
    return key in target;  

    // return Reflect.has(target, key);  // proxy配合Reflect使用
  }
});

'b' in objProxy     // 查看objProxy中是否有这个属性 
delete objProxy.b   // 删除objProxy中的b属性


结果:

image.png