Proxy

作者: 感觉不错哦 | 来源:发表于2019-07-17 17:49 被阅读0次

    如果你平时有关注 Vue 的进展的话,可能已经知道了在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作

    Proxy它是一个构造函数,返回一个代理对象Proxy,主要用于从外部控制对对象内部的访问。

    使用 Proxy 的好处是:对象只需关注于核心逻辑,一些非核心的逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)可以让 Proxy 来做。从而达到关注点分离,降级对象复杂度的目的。

    代理允许你拦截在目标对象上的底层操作(Object下的许多原型方法),而这原本是 JS 引擎的内部能力。拦截行为使用了 一个能够响应特定操作的函数(被称为陷阱)
    外部如何去影响对象内部的访问呢,先创建一个简单的代理
        //目标对象        句柄对象
        var target = {}, handler = {};
        var proxy = new Proxy(target, handler);
    

    那么Proxy、target、handler这三者之间有什么关系呢?Proxy的行为很简单:将Proxy的所有内部方法转发至target 。即调用Proxy的方法就会调用target上对应的方法。那么handler又是用来干嘛的?handler的方法可以覆写任意代理的内部方法(一个或多个陷阱函数的对象)。 外界每次通过Proxy访问target 对象的属性时,就会经过 handler 对象,因此,我们可以通过重写handler对象中的一些方法来做一些拦截的操作。

        //最简单粗暴的proxy
        let target={}
        let proxy=new Proxy(target,{})
    
        proxy.name = "proxy"; 
        console.log(proxy.name); // "proxy" 
        console.log(target.name); // "proxy"
        target.name = "target";
        console.log(proxy.name); // "target" 
        console.log(target.name); // "target"
        console.log(proxy,target)//Proxy {name: "target"} {name: "target"}
    

    之前说了哈Proxy返回一个Proxy对象

    该例中的 proxy 对象将所有操作直接传递给 target 对象,当然没有陷阱函数的代理没什么用

    使用 set 陷阱函数验证属性值

             var a={
                    _year:2019,
                    get year(){
                        return this._year
                    },
                    set year(newValue){
                        this._year=newValue
                    }
                }
    

    可以区分一下写法,ES5 set支持一个参数,代理中的set支持四个参数

        trapTarget :将接收属性的对象(即代理的目标对象);
        key :需要写入的属性的键(字符串类型或符号类型);
        value :将被写入属性的值;
        receiver :操作发生的对象(通常是代理对象)//很多时候可以省略
    

    简单的demo

     let target={
              name:'lwj'
          }
    
          let proxy=new Proxy(target,{
              set(trapTarget, key, value, receiver){
                  if(!trapTarget.hasOwnProperty(key)){ //如果已存在的属性,绕过
                      if(isNaN(value)){
                        throw new TypeError("Property must be a number.");
                      }
                  }
                  //添加属性 
                  return Reflect.set(trapTarget, key, value, receiver)
                 //这里也可以改写 不使用上方的代码  函数还是尽量return一下 当然建议使用上方的方法
                //trapTarget[key]=value  
               //   return true
              }
          })
          proxy.count=1
          console.log(proxy.count) //1
          console.log(target.count) //1
          proxy.name='lwj'
          console.log(proxy.name)//lwj
          console.log(target.name) //lwj
          proxy.anotherName = "proxy"; //报错
    

    set 代理陷阱允许你在写入属性值的时候进行拦截,而 get 代理陷阱则允许你在读取属性 值的时候进行拦截。

    get陷阱函数参数借鉴了 set 陷阱函数的参数,只有一个明显的不同,也就是没有使用 value

          let target={
              name:'lwj'
          }
          console.log(target.age)//undefined
    

    由于js语言的特殊,属性是不会报错的,然而有时候我们不希望存在这个问题

          let proxy=new Proxy({},{
              get(tarpTarget,key,receiver){
                  //不存在此属性
                  if(!(key in receiver)){
                    throw new TypeError("Property " + key + " doesn't exist.");
                  }
                  return Reflect.get(tarpTarget,key,receiver);
              }
          })
          proxy.name='lwj'
          console.log(proxy.name)
          console.log(proxy.names) //报错 Property names doesn't exist.
    

    这里使用receiver是因为receiver自带has陷阱函数,使用 trapTarget 会跳过 has 陷阱函数

    写has方法之前写一个数据监听,如果你能看懂的话很棒,看不懂还是再看看前面!
         let onWatch=(obj,sets,gets)=>{
             let handler={
                 get(tarpTarget,key,receiver){
                     gets(tarpTarget,key)
                     return Reflect.get(tarpTarget,key,receiver)
                 },
                 set(tarpTarget,key,value,receiver){
                    sets(value,key) 
                    return Reflect.set(tarpTarget,key,value,receiver)
                 }
             }
             return new Proxy(obj, handler)
         }
    

    就不打注释了,自己理解的好

         let obj={a:1}
         let p=onWatch(obj,
           (v,property)=>{
            console.log(`监听到属性${property}改变为${v}`)
           },
           (target,property)=>{
            console.log(`'${property}' = ${target[property]}`)
           }
         ) 
         p.a = 2 // 监听到属性a改变
         p.a // 'a' = 2
    

    之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了

       get(target,key,receiver){s
            gets(target, key)
            // 这句判断代码是新增的
            if (typeof tarpTarget[key] === 'object' && tarpTarget[key] !== null) {
                return new Proxy(tarpTarget[key],handler);
            } else {
                return Reflect.get(tarpTarget,key);
            }
        }
    

    Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象(陷阱函数)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。比如Math对象

    has陷阱函数

    trapTarget :需要读取属性的对象(即代理的目标对象);
    key :需要检查的属性的键(字符串类型或符号类型)。

    let target={
        value:10,
        name:'lwj'
    }
    
    let proxy=new Proxy(target,{
        has(trapTarget,key){
            if(key==='value'){
                return false
            }else{
                return Reflect.has(trapTarget,key)
            }
        }
    })
    console.log("value" in proxy); // false
    console.log("name" in proxy); // true
    console.log("toString" in proxy); // true
    

    使用 deleteProperty 陷阱函数避免属性被删除

    delete 运算符能够从指定对象上删除一个属性,在删除成功时返回 true ,否则返回 false

    var obj={
        name:'lwj'
    }
    Object.defineProperty(obj,'age',{
        value:18,
        configurable:false
    })
    
    console.log("name" in obj) //true
    console.log('age' in obj)  //true
    let result= delete obj.name
    console.log(result) //true
    console.log("name" in obj) //false
    let results= delete obj.age
    console.log(results) // false
    console.log('age' in obj)  //true
    

    deleteProperty 陷阱函数会在使用 delete 运算符去删除对象属性时下被调用,并且会被传 入两个参数:
    trapTarget :需要删除属性的对象(即代理的目标对象);
    key :需要删除的属性的键(字符串类型或符号类型)。

    let target={
        name:'lwj',
        age:18
    }
    
    let proxy=new Proxy(target,{
        deleteProperty(trapTarget,key){
            if(key==="age"){
                return false
            }else{
                return Reflect.deleteProperty(trapTarget,key)
            }
        }
    })
    
    console.log("age" in proxy); // true
    let result1 = delete proxy.age
    console.log(result1); // false
    console.log("age" in proxy); // true
    console.log("name" in proxy); // true
    let result2 = delete proxy.name
    console.log(result2); // true
    console.log("name" in proxy); // false
    
    写下个陷阱函数之前先弥补两个对象下的方法getPrototypeOf(),setPrototypeOf(),获取原型设置原型
    let person={
        getting(){
            return 'Hello'
        }
    }
    
    let dog={
        getting(){
            return 'Hi'
        }
    }
    
    let friend = Object.create(person)
    console.log(friend.getting());//Hello
    console.log(Object.getPrototypeOf(friend) === person); // true
    //原型设置为dog
    Object.setPrototypeOf(friend, dog);
    console.log(friend.getting());//Hi
    console.log(Object.getPrototypeOf(friend) === dog); // true
    

    寄生组合继承就是利用了Object.create方法,将父类的原型赋值给了子类,既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

    function Person(age){
        this.age=age
    }
    function Child(age){
        Person.call(this,age)
    }
    Child.prototype=Object.create(Person.prototype,{
        constructor:{
            value:Child,
            enumerable: false,
            writable: true,
            configurable: true
        }
    })
    const child=new Child(18)
    

    原型代理的陷阱函数setPrototypeOf() getPrototypeOf()

    原型代理的陷阱函数类似上方,也有两个,同样也可以利用Reflect反射,参数:
    trapTarget :需要设置原型的对象(即代理的目标对象);
    proto :需用被用作原型的对象。
    getPrototypeOf 陷阱函数只接受 trapTarget 参数

    这些陷阱函数受到一些限制。首先, getPrototypeOf 陷阱函数的返回值必须是一个对象或者 null ,其他任何类型的返回值都会引发“运行时”错误。对于返回值的检测确保了 Object.getPrototypeOf() 会返回预期的结果。类似的, setPrototypeOf 必须在操作没有成 功的情况下返回 false ,这样会让 Object.setPrototypeOf() 抛出错误;而若 setPrototypeOf 的返回值不是 false ,则 Object.setPrototypeOf() 就会认为操作已成 功。

    先看代码再看上面文字容易理解
    let target = {};
    let proxy=new Proxy(target,{
        getPrototypeOf(trapTarget){
            return null
        },
        setPrototypeOf(trapTarget,proto){
            return false
        }
    })
    
    let targetProto = Object.getPrototypeOf(target);
    let proxyProto = Object.getPrototypeOf(proxy);
    console.log(targetProto === Object.prototype); // true
    console.log(proxyProto === Object.prototype); // false
    console.log(proxyProto); // null
    
    Object.setPrototypeOf(target, {}); //成功
    
    Object.setPrototypeOf(proxy, {}); //报错
    

    使用 target 对象作为参数调用 Object.getPrototypeOf() 会返回一个对象值;而使用 proxy 对象调用该方法则会返回 null ,因为 getPrototypeOf 陷阱函数被调用了。类似的,使用 target 去调用 Object.setPrototypeOf() 会成功;而由于 setPrototypeOf 陷阱函数的存在,使用 proxy 则会引发错误。

    let target = {};
    let proxy=new Proxy(target,{
        getPrototypeOf(trapTarget){
            return Reflect.getPrototypeOf(trapTarget);
        },
        setPrototypeOf(trapTarget,proto){
            return Reflect.setPrototypeOf(trapTarget, proto);
        }
    })
    

    如此就可以了,俩者方法看起来非常相似但是也有差异,有兴趣可以百度,这里简单过一下

    首先, Object.getPrototypeOf() 与 Object.setPrototypeOf() 属于高级操作,而 Reflect.getPrototypeOf() 与 Reflect.setPrototypeOf() 属于底层 操作,允许开发者访问 [[GetPrototypeOf]] 与 [[SetPrototypeOf]]

    Reflect.getPrototypeOf() 方法在接收到的参数不是一个对象时会抛出错误,而 Object.getPrototypeOf() 则会在操作之前先将参数值转换为一个对象。

     let result1 = Object.getPrototypeOf(1);
     console.log(result1 === Number.prototype); // true
     Reflect.getPrototypeOf(1); //报错
    

    对象可扩展性的陷阱函数

    对象是否是可扩展的(是否可以在它上面添加新的属性)

    Object.isExtensible(obj); // === true
    

    大多数基本对象都是可扩展的

        // ...可以变的不可扩展.
        Object.preventExtensions(obj);
        Object.isExtensible(obj); // === false
    

    密封对象与冻结对象也不可扩展
    Object.seal({}); Object.freeze({});

        let target={}
    
        let proxy=new Proxy(target,{
            isExtensible(trapTarget){
                return Reflect.isExtensible(trapTarget);
            },
            preventExtensions(trapTarget) {
                //return false 即不会改变代理对象的可扩展性
                return Reflect.preventExtensions(trapTarget);
            }
        })
        console.log(Object.isExtensible(target)); // true
        console.log(Object.isExtensible(proxy)); // true
        Object.preventExtensions(proxy);
        console.log(Object.isExtensible(target)); // false
        console.log(Object.isExtensible(proxy)); // false
    

    属性描述符的陷阱函数

    defineProperty(trapTarget, key, descriptor)
    getOwnPropertyDescriptor(trapTarget, key)

    descriptor :为该属性准备的描述符对象

    ES5 最重要的特征之一就是引入了 Object.defineProperty() 方法用于定义属性的特性。在 JS 之前的版本中,没有方法可以定义一个访问器属性,也不能让属性变成只读或是不可枚 举。而这些特性都能够利用 Object.defineProperty() 方法来实现,并且你还可以利用 Object.getOwnPropertyDescriptor() 方法来检索这些特性。

      var obj={
          name:'lwj'
      }
      Object.defineProperty(obj,'age',{
          value:18,
          enumerable:true,
          writable:true,
          configurable:true
      })
    
     console.log(Object.getOwnPropertyDescriptor(obj,'name')) ===> descriptor
     descriptor : {value: "lwj", writable: true, enumerable: true, configurable: true}
    
     let proxy=new Proxy({},{
         defineProperty(trapTarget,key,descriptor){
             //此处可以做拦截  比如:
            //  if (typeof key === "symbol") { return false; }
            return Reflect.defineProperty(trapTarget, key, descriptor);
         },
         getOwnPropertyDescriptor(trapTarget,key){
            return Reflect.getOwnPropertyDescriptor(trapTarget, key);
         }
     })
    
     Object.defineProperty(proxy,'age',{
         value:18
     })
     console.log(proxy.age) //18
     let descriptor = Object.getOwnPropertyDescriptor(proxy, "age");
     console.log(descriptor.value);//18
    

    ownKeys 陷阱函数

    这个函数比较大哥,相当于调用了Object.keys() 方法、 Object.getOwnPropertyNames() 方法、 Object.getOwnPropertySymbols() 方法与 Object.assign() 方法

       let proxy=new Proxy({},{
           ownKeys(trapTarget){
               //返回一个数组 
            return Reflect.ownKeys(trapTarget).filter( key =>{ //这里的key是返回的每一项
                return typeof key!=='string'||key[0]!=='_' //过滤 带下划线的属性
            });
           }
       })
       let nameSymbol = Symbol("name");
       //增加属性
       proxy.name = "proxy";
       proxy._name = "private";
       proxy[nameSymbol] = "symbol";
    
       //获取返回数组
       let names = Object.getOwnPropertyNames(proxy),
       keys = Object.keys(proxy),
       symbols = Object.getOwnPropertySymbols(proxy);
       console.log(names)  //name
       console.log(keys)   //name
       console.log(symbols)//[Symbol(name)]
    

    使用 apply 与 construct 陷阱函数的函数代理

    在所有的代理陷阱中,只有 apply 与 construct 要求代理目标对象必须是一个函数

    函数被调用是执行 apply 陷阱函数
    trapTarget :被执行的函数(即代理的目标对象);
    thisArg :调用过程中函数内部的 this 值;
    argumentsList :被传递给函数的参数数组。

    当使用 new 去执行函数时, construct 陷阱函数会被调用并接收到下列两个参数:
    trapTarget :被执行的函数(即代理的目标对象);
    argumentsList :被传递给函数的参数数组。

         let target=function(){return 1}
         let proxy=new Proxy(target,{
             apply(trapTarget,thisArg,argumentList){
                return Reflect.apply(trapTarget,thisArg,argumentList);
             },
             construct(trapTarget,argumentList){
                 return Reflect.construct(trapTarget,argumentList)
             }
         })
         // 使用了函数的代理,其目标对象会被视为函数
         console.log(typeof proxy); // "function"
         console.log(proxy()); // 1
         var instance = new proxy();
         console.log(instance instanceof proxy); // true
         console.log(instance instanceof target); // true
    

    apply 与 construct 陷阱函数在函数的执行方式上开启了很多的可能性。例如,假设你想要 保证所有参数都是某个特定类型的,可使用 apply 陷阱函数来进行验证:

         function sum(...values){
             return values.reduce((prev,current)=>prev+current)
         }
         let sumProxy=new Proxy(sum,{
             apply(trapTarget,thisArg,argumentList){
                 argumentList.forEach(el=> {
                     //参数必须为num
                     if(typeof el!=='number'){
                        throw new TypeError("All arguments must be numbers.");
                     }
                 });
                 return Reflect.apply(trapTarget,thisArg,argumentList);
             },
             construct(trapTarget,argumentList){   //不允许创建实例对象 
                throw new TypeError("This function can't be called with new.");
             }
         })
         console.log(sumProxy(1, 2, 3, 4)); // 10
         console.log(sumProxy(1, "2", 3, 4));//报错
         let result = new sumProxy();//报错
    

    同样可以再construc陷阱函数中对argumentList进行限制

    ES6为我们提供了一个元属性(元属性指的是“非对象”(例如 new ) 上的一个属性) new.target,主要可以限制构造函数必须使用new

         function Person(name){
             if(typeof new.target!=='undefined'){
                 this.name=name //使用了new
             }else{
                throw new Error("You must use new with Person.")
             }
         }
         var person = new Person("lwj");
         var notAPerson = Person('age') //报错
    

    一般情况下构造函数是必须带new的
    在new一个实例的过程中,其实是执行了如下的步骤

    1、声明一个中间对象
    2、将该中间对象的原型指向构造函数的原型
    3、将构造函数中的this指向该中间对象
    4、返回该中间对象,即返回实例对象
    现在可以利用apply与construct陷阱函数使其无需new也可以

        function Num(...values){ //构造函数首字母大写 
            if(typeof new.target==='undefined'){
                throw new TypeError("This function must be called with new.");
            }
            this.values=values
        }
    
        let NumbersProxy=new Proxy(Num,{
            apply(trapTarget,thisArg,argumentList){
                return Reflect.construct(trapTarget,argumentList)//在函数调用的时候执行 construct函数
            }
        })
        let instance = NumbersProxy(1, 2, 3, 4); //未报错
        console.log(instance.values); // [1,2,3,4]
    

    同样的道理可以return new trapTarget(...argumentList);

    可被撤销的代理

    Proxy.revocable() 方法来创建一个可被撤销的代理,该方法接受的参数与 Proxy 构造器的相同:一个目标对象、一个代理处理器,而返回值是包含下列属性的一个对 象:
    proxy :可被撤销的代理对象;
    revoke :用于撤销代理的函数。

    revoke() 函数被调用后,就不能再对该 proxy 对象进行更多操作

       let target={
           name:'lwj'
       }
       //解构赋值操作
       let{proxy,revoke}=Proxy.revocable(target,{})
       console.log(proxy.name); // "target"
       revoke();
       console.log(proxy.name);//报错
    

    解决一个数组的小问题

         let colors = ["red", "green", "blue"];
         console.log(colors.length); // 3
         colors[3] = "black";
         console.log(colors.length); // 4
         console.log(colors[3]); // "black"
         colors.length = 2;
         console.log(colors.length); // 2
         console.log(colors[3]); // undefined
         console.log(colors[2]); // undefined
         console.log(colors[1]); // "green"
    

    当 colors[3] 被赋值时, length 属性被自动增加到 4 ;
    当 length 属性被设置为 2 时,数组的最后两个元素被自动移除了。
    当想要重现内置数组的工作方式时,仅需模拟这两个行为即可。

    检测数组的索引

    数组最多可以包含43亿左右的项(4 294 967 295),然而在JavaScript中数组的索引不能超过一个数,也就是2的32次方-1,只有小于等于这个数才能作为数组的索引(最大的安全整数是2的53次方-1,也就是Number.MAX_SAFE_INTEGER)

         //检索是否符合数组索引 可以则 isArrayIndex(key)返回true
         function toUint32(value) { 
             return Math.floor(Math.abs(Number(value))) % Math.pow(2,32)
         }
         function isArrayIndex(key) {
             let numerickey=toUint32(key)
             return String(numerickey)==key && numerickey<(Math.pow(2,32)-1)
          }
    

    模拟类似的操作

         //检索是否符合数组索引
         function toUint32(value) { 
             return Math.floor(Math.abs(Number(value))) % Math.pow(2,32)
         }
         function isArrayIndex(key) {
             let numerickey=toUint32(key)
             return String(numerickey)==key && numerickey<(Math.pow(2,32)-1)
          }
        
         function createMyArray(length=0){
             //缩写{length:length}
             return new Proxy({length},{
                 set(trapTarget,key,value){
                    let currentLength = Reflect.get(trapTarget, "length");
                    //特殊情况
                    if (isArrayIndex(key)){
                        let numericKey = Number(key);
                        if (numericKey >= currentLength) {
                            Reflect.set(trapTarget, "length", numericKey + 1); //未做其他处理,只是递增
                        }
                    }
                    return Reflect.set(trapTarget, key, value);
                 }
             })
         }
    
         let colors = createMyArray(3); //length:3
         console.log(colors.length); // 3
         colors[0] = "red"; 
         colors[1] = "green";
         colors[2] = "blue";
         console.log(colors); // Proxy {0: "red", 1: "green", 2: "blue", length: 3}
         colors[3] = "black";
         console.log(colors.length); // 4
         console.log(colors[3]); // "black"
    

    其他情况可以自己模拟一下!!!

    相关文章

      网友评论

          本文标题:Proxy

          本文链接:https://www.haomeiwen.com/subject/sdozkctx.html