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

定义

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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花

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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去送.于是上述代码可以改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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类,这种形式就叫做虚拟代理

1
2
3
4
5
6
7
8
var B={
receiveFlower:function(){
A.listenGoodMood:function(){
var flower=new Flower()
A.receiveFlower(flower)
}
}
}

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

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

1
2
3
4
5
6
7
8
9
var myImage=(function(){
var imgNode=document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc:function(src){
imgNode.src=src;
}
}
})()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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代理模式的意义

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法,在客户看来,代理本体和代理对象时一样的.
另外值得一提的是,如果代理对象和本体对象为一个代码(函数也是对象),那么则可以认为它们也具有一致的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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的时候,它对应的文件就会被同步到另一台备用服务器上面

1
2
3
4
5
6
7
8
9
10
11
<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绑定点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
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来收集一段时间之内的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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之前

1
2
3
4
5
6
7
8
9
10
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造成重复加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 缓存代理的例子——计算乘积

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

1
2
3
4
5
6
7
8
9
10
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

现在加入缓存代理函数

1
2
3
4
5
6
7
8
9
10
11
12
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请求是异步的 不可能直接存储到缓存函数中.
这里作者没给出函数,本渣渣来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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用高阶函数动态创建代理

1
2
3
4
5
6
7
8
9
10
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,这样就可以多重调用了.完整代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**************** 计算乘积 *****************/
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用的比较多的是虚拟代理和缓存代理.
实际上在我们编写业务代码的时候,往往不需要去预先猜测是否需要使用缓存代理模式,当真正发现不方便直接访问某个对象的时候,再编写也不迟.