模拟一下Proxy代理

173 阅读4分钟

手写Proxy

defineProperty的用法

Object.Property主要实现的是对对象属性描述符的管理,属性描述符一共分元属性和存取器两种。

其中元属性为:

  • value 控制值
  • writable 控制是否可写
  • enumerable 控制是否可遍历
  • configurable 控制是否能改变属性描述符

存取器为:

  • getter
  • setter 如果设置了存取器,那么元属性value就无效。

一般存取器是这样用的

const obj={
   money:1000,
   get m(){return this.money},
   set m(newValue){this.money=newValue}
}
obj.m //1000
obj.m = 2000
onj.m //2000
obj.money //1000
obj.money=3000
obj.m //3000

了解了基本原理,下面就通过Object.defineProperty来返回一个代理。

目标:

  • 对该代理的修改操作,都将改变目标对象的属性
  • 该代理有数据拦截功能,现在我也不知道拦截了能干啥,就打印一段话吧

代码

const target={
  name:'qiuyanxi',
  message:{
    id:123,
    age:28,
  }
}

function myProxy(target){
  const obj={}
  Object.keys(target).forEach((key,)=>{
    Object.defineProperty(obj,key,{
      get(){
        console.log(`get了${key}`)
        return target[key]
      },
      set(newValue){
        console.log(`set了${key}`)
        target[key]=newValue
      }
    })
  })
  return obj
}
const p=myProxy(target)
console.log(p.name)
p.name='123456'
console.log(target.name)

以下是打印台信息

"get了name"
"qiuyanxi"
"set了name"
"123456"

可以看到我们设置了p.name后,target.name也跟着改变了,同时也出现拦截下来的信息。

不过上面的代码并没有把p.message也改成拦截模式,所以呢我们还需要使用递归来把数据类型为object的属性也设置成拦截器模式

const target = {
  name: "qiuyanxi",
  message: {
    id: 123,
    age: 28,
    mes: { name: "jack" } //这里又加了一层嵌套
  }
};

function myProxy(target) {
  if (typeof target === "object") {
    const obj = {};
    for (let k in target) {
      if (typeof target[k] !== "object") {
        Object.defineProperty(obj, k, {
          get() {
            return target[k];
          },
          set(newValue) {
            target[k] = newValue;
          }
        })
      } else {
        obj[k] = myProxy(target[k])
      }
    }
    return obj;
  }
}
const p = myProxy(target);

来看看打印台看代理的属性

每一层都已经做上了拦截器

再来看看原来的target属性有没有设置拦截器 一点都没有影响,很好。

优化代码

const target = {
  name: "qiuyanxi",
  message: {
    id: 123,
    age: 28,
    mes: { name: "jack" }, //这里又加了一层嵌套
  },
};

function myProxy(target) {
  const obj = {};
  for (let k in target) {
    if (typeof target[k] !== "object") {
      define(obj, k, target);
    } else {
      obj[k] = myProxy(target[k]);
    }
  }
  return obj;
}

function define(obj, key, target) {
  Object.defineProperty(obj, key, {
    get() {
      return target[key];
    },
    set(newValue) {
      target[key] = newValue;
    },
  });
}
const p = myProxy(target);

测试用例

p.name='我改了代理的name'
p.message.id='我改了代理的message的id'
p.message.mes.name='我改了代理的message的mes的name'
console.assert(p.name===target.name,'报错说明拦截器没用')
console.assert(p.message.id===target.message.id,'报错说明拦截器没用')
console.assert(p.message.mes.name===target.message.mes.name,'报错说明拦截器没用')

target.name='我改了目标对象的name'
target.message.id='我改了目标对象的message的id'
target.message.mes.name='我改了目标的message的mes的name'
console.assert(p.name===target.name,'报错说明拦截器没用')
console.assert(p.message.id===target.message.id,'报错说明拦截器没用')
console.assert(p.message.mes.name===target.message.mes.name,'报错说明拦截器没用')

缺陷

虽然上面已经实现了一个代理,但是有个缺陷,那就是没有办法控制数组,不信我们用一个数组试一试

const array=[1,2,3]
const p = myProxy(array);
console.log(p)
p[0]=666 //修改代理的第0个值
console.log(array[0]) //666
p.push(888) //TypeError: p.push is not a function

数组的方法虽然能保持已有数组的同步修改,但是缺点是不能通过代理push(以及其他数组方法),难怪vue2用了自己做的数组方法

Proxy构造函数

下面学习一下新增的Proxy构造函数

基本用法

const p = new Proxy(target, handler)
  • target 目标对象

  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

const array=[1,2,3]
const p=new Proxy(array,{})
p.push(99999)
console.log(array) // [1, 2, 3, 99999]

上面的语法p是生成的proxy对象,可以直接对数组array进行代理,而且我push一个数字之后,array就马上会变化。

使用proxy拦截示例

const obj={}
const obj2=new Proxy(obj,{
   set(obj,props,value){
      if(props==='name'){
         obj.name=value
      }else{
         throw new Error('不能设置其他属性')
      }
   }
})

obj2.name=123
obj2.aaa='9999' // Error: 不能设置其他属性
console.log(obj) //{name:123}

代理对象示例

const target = {
  name: "qiuyanxi",
  message: {
    id: 123,
    age: 28,
    mes: { name: "jack" } //这里又加了一层嵌套
  }
};
const p = new Proxy(target,{});//没错,就一行。。。

可以用我上面的测试用例测一下,proxy实现了完全掌控,连递归都不放过,真的香。 不过上面就是纯代理,生成的p是一个proxy对象,它的任何修改都会影响到原数据。

增加数据拦截

const target = {
  name: "qiuyanxi",
  message: {
    id: 123,
    age: 28,
    mes: { name: "jack" } //这里又加了一层嵌套
  }
}
function myProxy(obj){
  for(let k in obj){
    if(typeof obj[k]==='object'){
      obj[k]=myProxy(obj[k])
    }
  }
  return new Proxy(obj,{
    get(o,p){
      if(p in o){
        console.log('get'+p)
        return o[p]
      }
    },
    set(o,p,v){
      console.log('set'+p)
      o[p]=v
    }
  })
}