Proxy代理

前面的话

Proxy 用于修改某些操作的默认行为,等同于在语言层面上做出修改,所以是一种“元编程”,即对编程语言进行编程。Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供一种机制可以对外界的访问进行过滤和改写。

创建简单代理

ES6 原生提供Proxy构造函数,使用new Proxy() 生成一个Proxy实例。Proxy构造函数需传递两个参数,一个是target参数(表示所要拦截的目标对象),一个是handler对象,用来定制拦截行为 。 如果handler没有设置任何拦截,那就等同于直接通向原对象。

let target = {};
let proxy = new Proxy(target, {});
proxy.name = 'xiaoqi';
console.log(target.name);// xiaoqi 
target.name = 'target';
console.log(target.name);// target
console.log(proxy.name);// target

这个例子,handler没有设置任何拦截,所有操作直接转发到目标对象。将“xiaoqi”赋值给proxy.name属性时会在目标对象上创建name属性,代理只是简单地将操作转发给目标,它不会存储这个属性。由于proxy.name和target.name引用的都是target.name. 因此二者的值相同,从而target.name设置新值后,proxy.name也一同变化

拦截方法
[set()]

set方法用于拦截某个属性的赋值操作。

set()接受4个参数

  • target : 代理目标对象
  • propKey: 要写入的属性键
  • value: 被写入的属性值
  • receiver: 操作发生的对象(通常是代理)

假定Person对象有一个age 对象,该属性应该是一个不大于200的整数,那么可以使用Proxy对象保证age的属性值符合要求

 let validetor  = {
    set: function(target, key, value) {
        if(key === 'age') {
            if(!Number.isInteger(value)) {
                throw new TypeError('The age is not an integer');
            }
        
        if(value > 200) {
            throw new RangeError('The age seems invalid');                
        }
    }
     // 对于age以外的属性,直接保存
     target[key] = value;
    }
};
let person = new Proxy({},validetor);
person.age = 100;
console.log(person.age);// 100 
//  person.age = 'young';  // TypeError: The age is not an integer

person.age = 300;// RangeError: The age seems invalid

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

[get()]

JS有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取的属性值。使用Proxy实例中的get()方法可以在读取属性时检测该属性是否存在。

get()方法用于拦截某个属性懂得读取操作。

get()方法有3个参数:

  • target: 被读取属性的源对象(代理目标)
  • propKey: 要读取的属性键
  • receiver:操作发生的对象

由于get不写入值,所有它复刻了set中的除value外的其它3个参数。

下面是一个拦截读取操作的例子:

   var person = {
     name: '张三'
 };
 var proxy = new Proxy(person, {
     get: function(target,propKey) {
         if(propKey in target) {
             return target[propKey];
      } else {
         throw new ReferenceError("propKey \" "+ propKey + " \"does not exist");
      }
  }
 });
  console.log(proxy.name);// 张三
  console.log(proxy.age);//  propKey " age "does not exist 

这个例子中,如果访问目标对象不存在的属性,会抛出一个错误。

get()方法可以继承:

    let proxy = new Proxy({}, {
       get(target, propKey, receiver) {
           console.log('GET ' + propKey);
           return target[propKey];
       }
   });
   let obj = Object.create(proxy);
   obj.name;// GET name

上面的例子中,obj对象的原型是Proxy实例对象。继承了实例对象的get方法。

使用get()拦截实现数组读取负数索引:

   function createArray(...elements) {
        let handler = {
            get(target, propKey, receiver) {
                let index = Number(propKey);
                if(index < 0);
                {
                propKey = String(target.length + index);
                }
                return  Reflect.get(target, propKey, receiver);
            }
          
        };
        let target = [];
        target.push(...elements);
        return new Proxy(target, handler);
    }
    let arr = createArray('a', 'b', 'c');
    console.log( arr[-1]);// c

使用get拦截实现一个生成各种DOM节点的通用函数dom:

      const dom = new Proxy({},{
       get(target, propKey) {
           return function(attrs, ...children) {
               const el = document.createElement(propKey);
               for(let prop of Object.keys(attrs)) {
                   console.log(prop);
                   el.setAttribute(prop, attrs[prop]);
               }
               for(let child of children) {
                   if(typeof child === 'string') {
                       child = document.createTextNode(child);
                   }
                   el.appendChild(child);
               }
               return el;
           }
       }
   });
   const el = dom.div({},
   'hello, my name is ',
   dom.a({href: '//example.com'},'Mark'),
   '. I like:',
   dom.ul({},
   dom.li({},'The web'),
   dom.li({},'Food'),
   dom.li({},'...actually that\'s it')
   )
   );
   document.body.appendChild(el);

上面的例子中,使用get()方法返回了一个生成DOM的函数。其中执行dom.div()时,代理目标是{},propKey是div,而dom.div()中的参数对应get方法中的参数,第一个参数为attrs,其他的参数为…children。其他的dom. a(),dom.ul(),dom.li()的执行过程与dom.div()一样。

如果一个属性不可配置(configurable)或不可写(writable),则该属性不能被代理,通过Proxy对象访问该属性将会报错。

    const target = Object.defineProperties({}, {
       foo: {
           value: 123,
           writable: false,
           configurable: false 
       }
   });
   const handler = {
       get(target, propKey) {
           return 'abc';
       }
   }
   const proxy = new Proxy(target, handler);
   proxy.foo;// 报错
[结合使用get和set方法]

有时我们会在对象上设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。

    var handler = {
       get (target, propKey) {
           invariant (propKey, 'get');
           return target[propKey];
       },
       set(target, propKey, value) {
           invariant(propKey, 'set');
           target[propKey] = value;
           return true;
       }
   };
   function invariant (propKey, action) {
       if(propKey[0] === '_') {
           throw new Error(`Invalid attempt to ${action} private "${propKey}" property`);
       }
   }
   var target = {};
   var proxy = new Proxy(target, handler) ;
   proxy.prop = 'b';
   console.log(proxy.prop); // b
   proxy._prop;//   Uncaught Error: Invalid attempt to get private "_prop" property
   proxy._prop = 'c';//  Uncaught Error: Invalid attempt to get private "_prop" property
[apply()]

apply方法拦截函数的调用、call和apply操作.

apply方法可以接受3个参数:

  • target:目标对象
  • object:目标对象的上下文对象(this)
  • args: 目标对象的参数数组。

一个简单的例子:

    var target  = function () {return 'I am the target'};
   var handler = {
       apply() {
           return 'I am the proxy'
       }
   }
   var p = new Proxy(target, handler);
   console.log( p()); // I am the proxy

p是Proxy实例,作为函数调用时(p())就会被apply方法拦截,返回一个字符串。

下面是另外的例子:

  var twice  = {
      apply (target, ctx, args) {
          return Reflect.apply(...arguments) * 2;
      }
  };
  function sum (left, right) {
      return left + right;
  }
  var proxy = new Proxy(sum, twice);
   console.log( proxy(1, 2));// 6
   console.log( proxy.call(null, 5, 6));// 22
   console.log( proxy.apply(null, [7, 8]));// 30

上面的例子中,proxy是Proxy实例对象,代理的是sum函数,当执行proxy函数(直接调用或apply和call调用),就会被apply方法拦截。

[has()]

has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in操作符。

has方法有两个参数:

  • target: 目标对象
  • propKey:要读取的属性键

下面的例子,使用has方法隐藏某些属性,使其不被in操作符发现:

     var handler = {
       has(target, propkey) {
           if(propkey[0] === '_') {
               return false;
           }
           return propkey in target;
       }
   }
   var target = {_prop: 'foo', prop: 'bar'};
   var proxy = new Proxy(target, handler);
   console.log( '_prop' in proxy);// false
   console.log( 'prop' in proxy);// true;

上面代码中,如果原对象的属性名的第一个字符是下画线。proxy.has就会返回false,从而就不会被in运算符发现。

如果原对象不可配置或禁止扩展,那么此时has拦截会报错:

   var obj = {a : 10};
   Object.preventExtensions(obj);
   var p = new Proxy(obj, {
       has(target, prop) {
            return false;
       }
   })
  console.log('a' in p) ;// 报错

上面的代码中,obj对象禁止扩展,使用has拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has方法就不得 “隐藏”(即返回false)目标对象的该属性。

has拦截对for…in循环不生效:

  let stu1 = {name: '张三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
    has(target, propkey) {
        if(propkey === 'score' && target[propkey] < 60) {
            console.log(`${target.name} 不及格`);
            return false;
        }
        return propkey in target;
    }
}
   let proxy1 = new Proxy(stu1, handler);
   let proxy2 = new Proxy(stu2, handler);
   console.log( 'score' in proxy1);//  张三不及格 false
   console.log('score' in proxy2);// true

    for(let a in proxy1) {
        console.log(proxy1[a]);
    }
    //  张三
    // 59
 
    for(let a in proxy2) {
        console.log(proxy2[a]);
    }
    //  李四
    //   99

由此可以得出has拦截只对in循环有效,对for…in循环不生效。导致不符合要求的属性没有排斥在for…in循环之外

[construct()]

construct方法用于拦截new命令,它接受两个参数:

  • target: 目标对象
  • args: 构建函数的参数对象
var p = new Proxy(function () {}, {
     construct: function(target, args) {
         console.log('called: ' + args.join(', ') );
         return {value: args[0] * 10}
     }
 })
console.log( (new p(1, 2, 3)).value);
// called: 1,2,3
//  10

注意:construct方法返回的必须是一个对象,否则会报错。
[deleteProperty()]

deleteProperty方法用于拦截delete操作。如果这个方法抛出错误或者返回false,当前属性就无法被delete命令。

它接受两个参数:

  • target: 目标对象
  • propKey:要读取的属性键
var handler = {
     deleteProperty(target, propKey) {
         invariant(propKey, 'delete');
         return true;
     }
 };
 function invariant (propKey, action) {
     if(propKey[0] === '_') {
         throw new Error(` Invalid attempt to ${action} private "${propKey}" property`)
     }
 }
 var target = {_prop: 'foo'};
 var proxy = new Proxy(target, handler);
 delete proxy._prop;//   Uncaught Error:  Invalid attempt to delete private "_prop" property

上面的例子中,deleteProperty方法拦截了delete操作符,当删除的属性名第一个字符为“_”时,抛出错误。

[注意]:目标对象自身不可配置(configurable)的属性不能被deleteProperty方法删除,否则会报错。

[defineProperty()]

defineProperty()方法拦截Object.defineProperty操作。

它接受三个参数:

  • target: 目标对象
  • propKey:要读取的属性键
  • propDesc: 属性描述
 var handler = {
           defineProperty:function(target,key,propDesc) {
               console.log(propDesc);
               target[key] = propDesc.value;
            //    return false;
           }
       };
       var target = {};
       var proxy = new Proxy(target,handler);
       proxy.foo = 'bar'
       console.log('proxy添加新的属性:',proxy);
     //  {value: "bar", writable: true, enumerable: true, configurable: true}
    // proxy添加新的属性: Proxy {foo: "bar"}

上面的例子中,可得出Proxy代理内的definneProperty方法return true/false并没有任何意义。

当目标对象添加属性的方法被defineProperty方法拦截,若不添加target[key] = descriptor.value,则不能添加属性。

[getOwnPropertyDescriptor()]

getOwnPropertyDescriptor()方法用来拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

它接受两个参数:

  • target: 目标对象
  • propKey:要读取的属性键
 var handler = {
        getOwnPropertyDescriptor(target, key) {
            if(key[0] === '_'){
                return;
            }
            return Object.getOwnPropertyDescriptor(target, key);
        }
    };
    var target = {_foo: 'bar', baz:'tar'};
    var proxy = new Proxy(target, handler);
    console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'));// undefined
    console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'));// undefined
    console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'));// {value: "tar", writable: true, enumerable: true, configurable: true}
[getPrototypeOf()]

getPrototypeOf()方法用来拦截获取对象原型。 它接受一个参数: target 目标对象。具体来说,用于拦截一下操作:

  • Object.prototype.proto
  • Object.prototype.isPrototypeOf(): 用于测试一个对象是否存在于另一个对象的原型链上
  • Object.getPrototypeOf(): 返回指定对象的原型(内部[[Prototype]]属性的值)
  • Reflect.getPrototypeOf(): 返回指定对象的原型(内部[[Prototype]]属性的值
  • istanceof(): 用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置
var proto = {};
   var p = new Proxy(proto, {
       getPrototypeOf(target) {
           return target;
       }
   });
   console.log(Object.getPrototypeOf(p) === proto);// true
       

上面的代码中,getPrototypeOf方法拦截Object.getPrototypeOf(),返回proto对象。

[注意]:getPrototypeOf方法的返回值必须是对象或者null,否则会报错。另外,如果目标对象不可扩展(extensible),getPrototypeOf方法必须返回目标对象的原型对象。

[isExtensible()]

isExtensible方法拦截Object.isExtensible()操作。

Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

它接受一个参数:

  • target: 目标对象
 var p = new Proxy({}, {
    isExtensible(target) {
        console.log('called');
        return true;
    }
});
console.log(Object.isExtensible(p));
// called
// true

上面的代码设置了isExtensible方法,在调用Object.isExtensible()时会输出called

[注意]:这个方法只能返回布尔值,否则返回值会被自动转为布尔值

这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。

Object.isExtensible(proxy) === Object.isExtensible(target);

下面是一个例子:

 var p = new Proxy({}, {
        isExtensible(target) {
            return false
        }
    });
    Object.isExtensible(p);// 报错
[ownKeys()]

ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作:

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

下面是拦截Object.keys()的例子

  let target = {
        a: 1,
        b: 2,
        c: 3
    };
    let handler = {
        ownKeys(target) {
            return ['a'];
        }
    };
    let proxy = new Proxy(target, handler);
    console.log( Object.keys(proxy));
    // ['a']

上面的代码拦截了对target对象的Object.keys()操作,只返回a,b,c三个属性中的a属性。

下面是一个拦截第一个字符为下划线的属性名:

     let target = {
        _bar: 'foo', 
        _prop: 'bar',
        prop: 'baz' 
    }
    let handler = {
        ownKeys(target) {
            return Reflect.ownKeys(target).filter(function(key){
               return  key[0] !== '_'
            } )
        }
    }
    let proxy = new Proxy(target, handler);
    for(let key of Object.keys(proxy)) {
        console.log(target[key]);
    }
   // baz

注意: 使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回

  • 目标对象上不存在的属性
  • 属性名为Symbol值
  • 不可遍历的属性
    let target = {
        a: 1,
        b: 2,
        c: 3,
        [Symbol.for('secret')]: '4'
    };
    Object.defineProperty(target, 'key', {
        enumerable : false,
        configurable:true, 
        writable: true, 
        value: 'static'
    });
    let handler = {
        ownKeys(target) {
            return ['a', 'd', Symbol.for('secret'), 'key']
        }
    };
    let proxy = new Proxy(target,handler);
    console.log( Object.keys(proxy));// ['a']

上面的代码中,为target对象添加了一个不可遍历的key属性,在handler处理程序中,ownKeys方法返回[‘a’, ‘d’, Symbol.for(‘secret’), ‘key’] 其中只有a不属于上面三个条件之一。其他的结果都过滤掉了。

下面是拦截方法Object.getOwnPropertyNames()的例子:

var p = new Proxy({}, {
     ownKeys(target){
         return ['a', 'b', 'c'];
     } 
 });
 console.log(Object.getOwnPropertyNames(p));
// ['a', 'b', 'c']

ownKeys方法返回的数组成员只能是字符串或Symbol值。如果有其他类型的值,或者返回的根本不是数组,就会报错。

   var obj = {}
 var p = new Proxy(obj, {
    ownKeys(target) {
        return [123, true, undefined, null, {}, []];
    }
 });
 Object.getOwnPropertyNames(p); // 报错

如果目标对象自身包含一个配置的属性则该属性必须被ownKeys方法返回,否则会报错:

 var obj = {};
 Object.defineProperty(obj, 'a', {
     configurable: false,
     enumerable: true, 
     value: 10
 });
  var p = new Proxy(obj, {
      ownKeys(target) {
          return ['b']
      }
  })
  Object.getOwnPropertyNames(p); // Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'

上面的代码中,obj对象的a属性时不可配置,这时ownKeys方法返回的数组之中必须包含原对象所有属性,且不能包含多余的属性。

[preventExtensions()]

Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。

preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。

它接受一个参数:

  • target: 目标对象

这个方法有一个限制,只有目标对象不可扩展时,即Object.isExtensible(proxy)为false,proxy.preventExtensions()才能返回true,否则会报错。

 var p = new Proxy({}, {
      preventExtensions(target) {
          return true;
      }
  });
  Object.preventExtensions(p); // 报错

上面的代码中,proxy.preventExtensions方法返回true,但此时Object.isExtensible(proxy)会返回true,因此报错。

为防止出现这个问题,通常要在proxy.preventExtensions方法中调用一次Object.preventExtensions,代码如下:

    var p = new Proxy({}, {
          preventExtensions(target) {
              console.log('called');
              Object.preventExtensions(target);
              return true;
          }
      })
      Object.preventExtensions(p); 
      // called
      // true
[setPrototypeOf()]

setPrototypeOf方法主要用于拦截Object.setPrototypeOf()方法.

它接受两个参数:

  • target: 目标对象
  • proto: 指定的对象原型

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null。

   var handler = {
      setPrototypeOf(target, proto) {
          throw new Error('Changing the prototype is forbidden');
      }
  }
  var proto = {};
  var target = function () {};
  var proxy = new Proxy(target, handler);
  Object.setPrototypeOf(proxy, proto);// Uncaught Error: Changing the prototype is forbidden

注意:该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展,setPrototypeOf方法不得改变目标对象的
原型。

[proxy.revocable()]

proxy.revocable()返回一个可取消的Prox实例。

  let target = {};
  let handler = {};
  let {proxy, revoke} = Proxy.revocable(target, handler);
  proxy.foo = 123;
  console.log(proxy.foo);// 123
  revoke();
  console.log(proxy.foo);// 报错

Proxy.revocable()方法返回一个对象,其中proxy是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。 当执行revoke函数后再访问Proxy实例,就会报错。

this问题

虽然Proxy可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下也无法保证与目标对象的行为一致。主要原因就是在Proxy代理的情况下,目标对象内部this关键字会指向Proxy代理

   const target = {
      m(){
          console.log(this === proxy);
      }
  }
  const handler = {};
  const proxy = new Proxy(target, handler);
  target.m()// false
  proxy.m()// true

上面的代码中,一旦proxy代理target.m,后者内部的this就指向proxy,而不是target。

下面一个例子,由于this指向的变化导致Proxy无法代理目标对象:

    const _name = new WeakMap();
  class Person {
      constructor(name) {
          _name.set(this, name);
      }
      get name() {
          return _name.get(this);
      }
  }
  const jane = new Person('Jane');
  console.log( jane.name);// Jane
  const proxy = new Proxy(jane, {});
  console.log( proxy.name);//  undefined

上面的代码中,目标对象jane的name属性实例保存在外部对象_names上面,给_name对象添加this键,保存name属性值。由于proxy.name访问时,this指向proxy,导致无法取到值。

此外,有些原生对象的内部属性只有通过正确的this才能获取,所以Proxy也无法代理这些原生对象的属性:

  const target = new Date();
  const handler = {};
  const proxy = new Proxy(target, handler);
  proxy.getDate();//  Uncaught TypeError: this is not a Date object

上面的代码中,getDate方法只能在Date对象实例上面获取,如果this不是Date对象实例就会报错。这时,this绑定原始对象就可以解决这个问题。

 const target = new Date('2019-08-20');
  const handler = {
      get(target, propKey) {
          if(propKey === 'getDate') {
              return target.getDate.bind(target);
          }
          return Reflect.get(target, propKey);
      }
  };
  const proxy = new Proxy(target, handler);
  console.log(proxy.getDate());// 20     
实例:web服务的客户端

Proxy对象可以拦截目标对象的任意属性,这使得它很合适用来编写Web 服务的客户端。

  const service = createWebService('http://example.com/date');
  service.employees().then(json => {
    const employees = JSON.parse(json);
    // ...
  })

上面的代码新建了一个web服务的接口,这个接口返回各种数据。Proxy可以拦截这个对象的任意属性,所以不用为每个数据写一个适配方法,只要写一个Proxy拦截即可。

  function  createWebService(baseUrl) {
      return new Proxy({}, {
          get(target, propkey, receiver) {
              return ()=> httpGet(baseUrl + '/' + propKey);
          }
      })
  }

同理,Proxy也可以用来实现数据库的ORM层。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值