JavaScript设计模式笔记-代理模式

定义

代码模式是为一个对象提供一个代用品或者占位符,以便控制对它的访问.比如最常见的就是生活中的经纪人与明星.代理模式的关键在于,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制这个对象的访问,客户实际上访问的是替身对象。

6.1 第一个例子——小明追MM方式

因为小明胆子小不敢直接送花给MM于是让MM与他的的好友B来送给MM先来看看不用代理模式的情况

var Flower=function(){} 
var xiaoming={ 
   sendFlower:function(target){ 
       var flower=new Flower(); 
       target.receiveFlower(flower) 
   } 
} 
var A={ 
   receiveFlower:function(flower){ 
       console.log('收到花'+flower) 
   } 
} 
xiaoming.sendFlower(A) 

接下来我们引入代理B,既小明通过B来送给A花

var Flower=function(){} 
var xiaoming={ 
   sendFlower:function(target){ 
       var flower=new Flower(); 
       target.receiveFlower(flower) 
   } 
} 

var B={ 
   receiveFlower:function(){ 
       A.receiveFlower(flower) 
   } 
} 

var A={ 
   receiveFlower:function(flower){ 
       console.log('收到花'+flower) 
   } 
} 
xiaoming.sendFlower(B) 

很显然,执行结果跟第一段代码一致,至此我们就完成了一个最简单代理模式.虽然看起来然并卵..但是我们再加一个条件只有A MM心情好的时候送花记录才会高,但是小明无法得知MM什么时候心情好,只有B可以得知,那么就可以让B去送.于是上述代码可以改成

var Flower=function(){} 
var xiaoming={ 
   sendFlower:function(target){ 
       var flower=new Flower(); 
       target.receiveFlower(flower) 
   } 
} 

var B={ 
   receiveFlower:function(flower){ 
       A.listenGoodMood(function(){ 
           A.receiveFlower(flower)     
       }) 
   } 
} 

var A={ 
   receiveFlower:function(flower){ 
       console.log('收到花'+flower) 
   }, 
   listenGoodMood:function(fn){ 
       setTimeout(function(){//两秒后心情变好 
           fn() 
       },2000) 
   } 
} 
xiaoming.sendFlower(B) 

6.2 保护代理与虚拟代理

在程序中new一个object开销是比较大的.那么我们可以等到MM心情好的时候再创建Flower类,这种形式就叫做虚拟代理

var B={ 
   receiveFlower:function(){ 
       A.listenGoodMood:function(){ 
           var flower=new Flower() 
           A.receiveFlower(flower) 
       } 
   } 
} 

6.3 虚拟代理实现图片预加载

在img图片加载的时候我们通常会提供一个菊花图来供他加载.单纯的加载则是这样

var myImage=(function(){ 
       var imgNode=document.createElement('img'); 
       document.body.appendChild(imgNode); 
       return { 
           setSrc:function(src){ 
               imgNode.src=src; 
           } 
       }    
})() 

如果网速过慢的话 比如5kb/s那么页面则有很长时间的空白.那么我们可以先加载一个菊花图来显示

var myImage=(function(){ 
       var imgNode=document.createElement('img'); 
       document.body.appendChild(imgNode); 
       return { 
           setSrc:function(src){ 
               imgNode.src=src; 
           } 
       }    
})() 

var proxyImage=(function(){ 
       var img=new Image; 
       img.onload=function(){ 
           myImage.setSrc(this.src) 
       } 
       return { 
           setSrc:function(src){ 
               myImage.setSrc("菊花图") 
               img.src=src 
           } 
       } 
   })() 
proxyImage.setSrc('http://xxx.com/i.png') 

使用代理模式就可以预先加载一张菊花图,等到真正的图片加载完毕后再还原.

6.4代理模式的意义

先来看看不用代理模式实现的预加载图片函数

var myImage=(function(){ 
       var imgNode=document.createElement('img'); 
       document.body.appendChild(imgNode); 
       var img=new Image; 
       img.onload=function(){ 
           imgNode.src=img.src 
       } 
       return { 
           setSrc:function(src){ 
               imgNode.src="菊花图"; 
               img.src=src 
           } 
       }    
})() 
myImage.setSrc('http://xxx.com/i.png') 

要明白代理模式的意义首先要明白一个面向对象设计原则——单一原则.单一原则指的是,就一个类(通常也包括对象和函数等)应该仅有一个引起它变化的原因.如果一个对象承担了多个职责,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计,当发生变化时,设计可能会遭到意外的破坏.职责被定义为’引起变化的原因’.上段代码中MyImage出了负责给img节点设置src外,还要负责预加载图片,我们在处理其中一个职责中,有可能会因为其强耦合性影响另一个职责的实现.另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放——封闭原则.如果几年以后网速将非常快,那么我们可能希望把预加载这个功能取消,但是那时候我们不得不改变MyImage对象了。实际上,我们需要的只是给img节点设置src,预加载图片仅仅是一个锦上添花的功能,如果把这个功能放在另一个类中,自然是一个非常好的方法,于是代理的作用就在这里体现出来了.

6.5 代理与本体接口的一致性

上一节说到如果几年以后网速将非常快,那么我们可能希望把预加载这个功能取消.要取消代理对象,可以选择直接请求本体,其关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理本体和代理对象时一样的.另外值得一提的是,如果代理对象和本体对象为一个代码(函数也是对象),那么则可以认为它们也具有一致的接口

var myImage=function(){ 
   var imgNode=document.createElement('img'); 
   document.body.appendChild(imgNode) 

   return function(src){ 
       imgNode.src=src 
   } 
}() 

var proxyImage=function(){ 
   var img=new Image(); 
    
   img.onload=function(){ 
        myImage(this.src) 
   } 

   return function(src){ 
       myImage("菊花图地址") 
       img.src=src 
   } 

}() 

proxyImage("xxx.png") 

6.6虚拟代理合并HTTP请求

假设我们在做一个文件同步的功能,当我们选中个checkbox的时候,它对应的文件就会被同步到另一台备用服务器上面

<body> 
   <input type="checkbox" id="1"></input>1 
   <input type="checkbox" id="2"></input>2 
   <input type="checkbox" id="3"></input>3 
   <input type="checkbox" id="4"></input>4 
   <input type="checkbox" id="5"></input>5 
   <input type="checkbox" id="6"></input>6 
   <input type="checkbox" id="7"></input>7 
   <input type="checkbox" id="8"></input>8 
   <input type="checkbox" id="9"></input>9 
</body> 

接下来给这些checkbox绑定点击事件

var synchronousFile=function(id){ 
   console.log('开始同步文件,id为:'+id); 
} 

var checkbox=document.getElementsByTagName('input'); 

for(var i=0,c;c=checkbox[i++]){ 
   c.onclick=function(){ 
       if(this.checked===true){ 
           synchronousFile(this.id) 
       } 
   } 
} 

但如果频繁的点击checkbox,那么造成的开销是非常巨大的.解决方案是我们可以通过一个代理函数proxySynchronousFile来收集一段时间之内的请求

var proxySynchronousFile=(function(){ 
   var cache=[];//保存一段时间内需要同步的ID 
   var timer; 
   return function(id){ 
       cache.push(id) 
       if(timer){//保证不会覆盖已经存在的定时器 
           return false 
       } 
       timer=setTimeout(function(){ 
           synchronousFile(cache.join('.'));//两秒后发送保存的需要同步的ID集合 
           clearTimeout(timer);//清空定时器 
           timer=null 
           cache.length=0;//清空ID集合 
       },2000) 
   } 
   })() 
   var checkbox = document.getElementsByTagName( 'input' ); 
   for ( var i = 0, c; c = checkbox[ i++ ]; ){ 
       c.onclick = function(){ 
           if ( this.checked === true ){ 
               proxySynchronousFile( this.id ); 
           } 
       } 
   }; 

6.7 虚拟代理在惰性加载中的应用

本书的作者曾经写过一个mini控制台的开源项目 miniConsole.js 在用户没按F2加载控制台之前,才会加载miniConsole.js 那么之前调用的console可以用虚拟代理来保存调用的miniConsole.js未加载真正的miniConsole.js之前

var cache=[]; 
var miniConsole={ 
   log:function(){ 
       var arg=arguments; 
       cache.push(function(){ 
               return miniConsole.log.apply(miniConsole,args) 
           }) 
   } 
} 
miniConsole.log(1) 

当用户按下F2开始加载miniConsole.js,同时还要防止重复按F2造成重复加载

var handler=function(ev){ 
   if(ev.keyCode===113){ 
       var srcipt=document.createElement('srcipt') 
       srcipt.onload=function(){ 
           for(var i=0;fn=cache[i++];){ 
               fn() 
           } 
       } 
   }; 
   srcipt.src='miniConsole.js'; 
   document.getElementsByTagName('head')[0].appendChild(script); 
   document.body.removeEventListener('keydown',handler);//只加载一次miniConsole.js 
} 
//miniConsole.log代码 
miniConsole={ 
   log:function(){ 
       //真正代码略 
       console.log(Array.prototype.join.call(arguments)) 
   } 
} 
document.body.addEventListener('keydown',handler,false) 

这里还可以用到分时函数来缓解真正调用的压力

var timeChunk=function(ary,fn,count){ 
   var obj, 
       t; 
   var len=ary.length; 
   var start=function(){ 
       //返回1个或者指定数字的最小值 
       for(var i=0;i<Math.min(count||1,ary.length);i++){ 
           var obj=ary.shift();//每次弹出一个 
           if(typeof fn==='function'){ 
               return fn(obj) 
           } 
           obj() 
       } 
   }; 
   return function(){ 
       t=setInterval(function(){ 
           if(ary.length===0){//如果所有函数都被执行完毕 
               return clearInterval(t) 
           } 
           start() 
       },200)//分批执行的时间间隔,也可以用参数的形式传入 
   } 
} 
srcipt.onload=function(){ 
    var renderFriendList =timeChunk(cache,null,8) 
           
} 

6.8缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运行的时候,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果.

6.8.1 缓存代理的例子——计算乘积

先创建一个用于求乘积的函数

var mult=function(){ 
   console.log('开始计算乘积') 
   var a=1; 
   for(var i=0,l=arguments.length;i<1;i++){ 
       a=a*arguments[i] 
   } 
   return a; 
} 
mult(2,3)//6 
mult(2,3,4)//24 

现在加入缓存代理函数

var proxyMult=(function(){ 
   var cache=[]; 
   return function(){ 
       var args=Array.prototype.join.call(arguments,",");//以逗号分隔 
       if(args in cache){ 
           return cache[args] 
       } 
       return cache[args]=mult.apply(this,arguments) 
   } 
})() 
proxyMult(1,2,3,4);//24 
proxyMult(1,2,3,4);//24 

6.8.2缓存代理用于ajax异步请求数据

因为ajax请求是异步的 不可能直接存储到缓存函数中.这里作者没给出函数,本渣渣来实现。

var ajaxName=function(id,fn){ 
   $.get("http://xxx.com/"+id,function(data){ 
           console.log(data) 
           fn(data) 
       }) 
} 

var proxyAjaxName=function(){ 
   var cache={}; 
   return function(id){ 
       if(id in cache){ 
           return cache[id] 
       } 
       return ajaxName(id,function(data){ 
           cache[id]=data 
       }) 
   } 
}() 

既然是异步请求 那么就得由异步请求来处理

6.9用高阶函数动态创建代理

var createProxyFactory=(function(fn){ 
   var cache=[]; 
   return function(){ 
       var args=Array.prototype.join.call(arguments,",");//以逗号分隔 
       if(args in cache){ 
           return cache[args] 
       } 
       return cache[args]=fn.apply(this,arguments) 
   } 
}) 

只需要变动一个地方 添加上fn参数和更改调用的函数为fn,这样就可以多重调用了.完整代码参考

 
/**************** 计算乘积 *****************/ 
var mult = function(){ 
   var a = 1; 
   for ( var i = 0, l = arguments.length; i < l; i++ ){ 
       a = a * arguments[i]; 
   } 
   return a; 
}; 
/**************** 计算加和 *****************/ 
var plus = function(){ 
   var a = 0; 
   for ( var i = 0, l = arguments.length; i < l; i++ ){ 
       a = a + arguments[i]; 
   } 
   return a; 
}; 
/**************** 创建缓存代理的工厂 *****************/ 
var createProxyFactory = function( fn ){ 
   var cache = {}; 
   return function(){ 
       var args = Array.prototype.join.call( arguments, ',' ); 
       if ( args in cache ){ 
           return cache[ args ]; 
       } 
       return cache[ args ] = fn.apply( this, arguments ); 
   } 
}; 

var proxyMult = createProxyFactory( mult ), 
proxyPlus = createProxyFactory( plus ); 
alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24 
alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24 
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10 
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10 

6.10 其他代理模式

  • 防火墙代理:控制网络资源访问,保护主体不让’坏人’接近
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在java中远程代理可以是另一个虚拟机中的对象
  • 保护代理:用于对象应该有不同访问权限的情况
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数
  • 写时复制代理:通常用于复制一个庞大对象的情况,写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行赋值操作,写时赋值代理是虚拟代理的一种变体,DLL是典型的运用场景

囧…貌似没JavaScript啥事..可能nodejs用的到

6.11 小结

在开发JavaScript用的比较多的是虚拟代理和缓存代理.实际上在我们编写业务代码的时候,往往不需要去预先猜测是否需要使用缓存代理模式,当真正发现不方便直接访问某个对象的时候,再编写也不迟.