Proxy基础知识

790 阅读7分钟
  • 本章节介绍一下Proxy部分,在此之前需要了解一下元编程

一. 元编程

  • 元编程是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为他们的数据,或者在运行时完成部分本应在编译时完成的工作。很多情况下与手工编写全部代码相比工作效率更高。
  • 编写元程序的语言称为元语言,被操纵的语言被称为目标语言,一门语言是自身的元语言的能力被称为反射!
  • 从ES5开始,js获得对Proxy,Reflect对象的支持,允许拦截并定义基本语言操作的自定义行为,例如(属性查找,赋值,枚举,函数调用等),借助 Proxy,Reflect这两个对象,可以在js进行元编程

二. 概述

1. 基本含义

  • Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于元编程(meta programming),即对编程语言进行编程
  • Proxy可以理解为在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截。
  • 因此可以对外界的访问进行过滤和改写,Proxy的本意就是代理,在js中就是表示用Proxy来代理某些操作,作为代理器
  • 一个简单的get读取操作拦截的例子
 // 声明一个拦截器对象,属性就是要监听的对象属性
 var handler={
  get:function(target,name){
   if(target.hasOwnProperty(name)){
    return `对象包含有${name}属性`
   }else{ 
    return `对象没有${name}属性`
   }
  }
 }
 // 给obj对象绑定拦截器,然后创建拦截器实例
 var obj={name:'yiye',f:()=>{}}
 var p=new Proxy(obj,handler)
 // 1. 直接使用Proxy实例的属性
 console.log(p.name);//对象包含有name属性
 console.log(p.age);//对象没有age属性
    // 1.2 给Proxy实例赋值(但是被拦截了,所以修改不生效,除非Proxy去修改)
    p.name="改变"
    console.log(p.name);//对象包含有name属性
 // 2.此时没有拦截函数,所以会提示不是函数
 console.log(p.f());//p.f is not a function
 // 3. 如果调用对象不存在的函数则会提示变量不是函数
 console.log(p.foo());//p.foo is not a function
  • 可以把proxy作为对象属性绑定到对象中 var obj={proxy:new Proxy(target,handler)}
  • proxy有也可以作为其他对象的原型对象,Object.create(obj)
 var handler={
  get:function(target,proxyKey){
   return 35
  }
 }
 var a={name:'hh'}
 // 通过obj.proxy.xxx的形式使用代理器
 // new Proxy(obj,handler)的形式创建代理器
 var obj={proxy:new Proxy(Object.create(a),handler)}
 console.log(obj.proxy.name);//35
 console.log(obj.proxy.age);//35

2. 参数

  • new Proxy(target,handler)接收两个参数,表示目标对象和内部监听对象,handler为{}空对象表示不设置拦截,相当于直接访问原对象
  • handler内部监听的方法为set的时候有三个参数,(target,name,value),target表示目标对象,name表示属性名,value表示值
  • handler监听器监听的方法不一样的时候,参数个数和含义也都不一样
  • 下面是一个监听set操作的例子
 // 声明一个拦截器
 var handler={
 set:function(target,name,value){
  target[name]="set+"+value;
 }
 }
 // 给obj对象绑定拦截器,然后创建拦截器实例
 var obj={name:'yiye',f:()=>{}}
 var p=new Proxy(obj,handler)
 // 1. 直接使用Proxy实例的属性
 console.log(p.name);//yiye
    // 1.2 给Proxy实例赋值
    p.name="改变"
    console.log(p.name);//set+改变

3.proxy作为一种设计模式

  • 程序设计中存在一种设计模式为代理模式,Proxy Pattern
  • 所谓的代理者是指一个类别可以作为其他东西的接口,代理者可以作为任何东西的接口:网络连接,内存中的大对象,文件或其他昂贵或无法复制的资源。
  • 垃圾回收机制中的引用计数方法就是使用了代理模式
  • 当一个对象被引用时,创建代理者,每个代理者都会引用到对象。而作用在代理者的运算会传递到对象中。一旦所有的代理者都不存在,那么对象就会被清除,也就是当成垃圾回收了

三.Proxy实例的方法

  • proxy实例的方法共有13种,在此只介绍使用较多的几种。

1. get()

  • get方法用于拦截对象属性的读取操作,接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
  • 访问目标对象不存在的属性,也会执行到proxy监听的get读取操作如果不存在拦截器,那么返回的是undefined
  • 监听的get方法可以继承!
 var a={name:'a'}
 var proxy=new Proxy(a,{
  get(target,name,value){
   if(target.hasOwnProperty(name)) return 'get+'+target[name]
   return '不存在该属性'
  }
 })
 var child=Object.create(proxy)
 console.log(child);//{name: "eee",__proto__:Proxy},此处的Proxy是监听器对象Proxy,而不是函数
 console.log(child.__proto__);//不存在该属性
 console.log(child.prototype);//不存在该属性
 // 当对象child不存在属性name的时候,指向原型对象的属性
 console.log(child.name);//get+a
 // 此时给对象添加属性name,那么就指向对象自身的属性
 child.name='eee'
 console.log(child.name);//eee
  • 使用proxy完成链式调用
 var pipe = function (value) {
  var funcStack = []; // 存储函数
  var oproxy = new Proxy({} , {
   get : function (pipeObject, fnName) {
    if (fnName === 'get') {
     return funcStack.reduce(function (val, fn) {
      return fn(val);
     },value);
    }
    funcStack.push(window[fnName]);
    return oproxy;  // 返回proxy代理器
   }
  });
  return oproxy; // 返回proxy代理器
 }

 var double = n => n * 2;
 var pow = n => n * n;
 var reverseInt = n => n.toString().split("").reverse().join("") | 0; // 数字反转
 // 相当于 reverseInt(pow(double(3)));//3*2=6,6*6=36,36的倒序为63
 console.log(pipe(3).double.pow.reverseInt.get) // 63

2. set()

  • set方法有四个参数,分别是目标对象,属性名,属性值,proxy实例本身,最后一个参数可选
  • 一个实例:age属性的值大于200就提示错误
 var obj={age:10,name:'yiye'}
 var person=new Proxy(obj,{
  set:function(newobj,name,val,pro){
   if(name==='age'){
    if(val>200){
     throw new Error("this age is too max")
    }
   }
   newobj[name]=val;// 更新
  }
 })
 console.log(person);//Proxy {age: 10, name: "yiye"}
 console.log(obj);//{age: 10, name: "yiye"}
 
 // 直接修改目标对象的属性
 obj.age=210
 console.log(obj);//{age: 210, name: "yiye"}
 console.log(person);//Proxy {age: 210, name: "yiye"}

 // 修改proxy代理对象的属性(也生效!)
 person.age=20;// 此时都不会报错,并且数值一直
 console.log(person);//Proxy {age: 20, name: "yiye"}
 console.log(obj);//{age: 20, name: "yiye"}
 // person.age=220;// 此时超过限制
 console.log(obj); //Uncaught Error: this age is too max
 console.log(person); //Uncaught Error: this age is too max

限制

  • 当对象的某个属性不可写,那么set方法的监听将失效
 var obj={age:10,name:'yi'}
 Object.defineProperty(obj,'foo',{
  writable:false,
  value:'foo'
 })
 var person=new Proxy(obj,{
  set:function(target,name,value,receiver){
   target[name]="set+"+value;
  }
 })
 console.log(person);//Proxy {age: 10, name: "yi", foo: "foo"}
 // 修改foo属性
 person.foo="f"
 // 但是是修改不生效,依旧为foo!
 console.log(person);//Proxy {age: 10, name: "yi", foo: "foo"}

 // 此时修改其他属性,会生效的!
 person.age=10000
 person.name='good'
 console.log(person);//Proxy {age: "set+10000", name: "set+good", foo: "foo"}

3. apply()

  • apply方法拦截的有三种,函数的调用!!!,call绑定,apply绑定!
  • 接受三个参数,分别是目标对象,目标对象的上下文,目标对象的参数数组
 // 1. 创建一个监听apply操作的代理器
 var sum=function(a,b){
  return a+b
 }
 var proxy=new Proxy(sum,{
  apply:function(target,ctx,args){
   console.log(target,ctx,args);
   /* 
   ƒ (a,b){
    return a+b
   } undefined (2) [1, 2]
    */
   return sum(...args);
  }
 })
 // 2.函数调用
 console.log(proxy(1,2));//3
 // 3. call调用
 console.log(proxy.call(null,2,3));//5
 // 4. apply调用
 console.log(proxy.apply(null,[4,5]));//9

三.Proxy.revocable()

  • Proxy.revocable()返回一个可取消的Proxy实例
  • 返回的是一个对象,对象的proxy属性是proxy实例,revoke属性是一个函数,可以取消proxy实例,执行revoke函数之后,再访问proxy实例,就会抛出一个错误
 var obj={age:22,name:'ww'}
 var handler={}
 var {proxy,revoke}=Proxy.revocable(obj,handler);
 console.log(proxy);//Proxy {age: 22, name: "ww"}
 // 执行revoke函数,取消代理
 revoke();
 console.log(proxy);//Proxy {}
 // 此时再访问proxy实例的属性会报错提示代理已被取消
 console.log(proxy.age);// Cannot perform 'get' on a proxy that has been revoked

四. this指向问题

  • proxy会进行代理,但是这种代理不会使得this指向改变
  • 如果在监听的方法中不对this指向做绑定,那么使用的是this指向的规则
 var obj={
  m:function(){
   console.log(this===proxy)
  }
 }
 var handler={}
 var proxy=new Proxy(obj,handler)
 // obj对象调用m属性方法,所以内部this指的是obj
 obj.m();//false
 // proxy代理器实例对象调用m属性方法,所以内部this指向的是proxy
 proxy.m();//true
  • 如果需要绑定this指向target目标对象,那么就
 const target = new Date('2015-01-01');
 const handler = {
  get(target, prop) {
   if (prop === 'getDate') {
    return target.getDate.bind(target);
   }
   return Reflect.get(target, prop);
  }
 };
 const proxy = new Proxy(target, handler);
 console.log(target.getDate());//1
 // 此时即使是proxy对象调用方法,属性内部的this依旧指向目标对象target
 console.log(proxy.getDate()) // 1