您的位置:首页 > Web前端 > JavaScript

浅析JavaScript设计模式——代理模式

2016-11-17 20:33 363 查看
代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问

好久没有写设计模式的文章了

今天要写的代理模式可能内容要多一些

代理模式非常有用,代理模式也分为很多很多种,我只谈一些常见并且简单的

思想其实都是一样的

代理这个词我们并不陌生,在生活中例子也很多,比如,经纪人…

当我们不方便直接访问一个对象或者访问对象不满足我们需求的时候

我们可以把这个对象的控制权交给一个代理对象,让它来进行处理

我们使用这个代理对象,代理对象使用本体对象

就是这样的关系



保护代理

保护代理用于控制不同访问权限的对象对目标对象的访问

什么意思呢?

我的理解是,保护代理可以帮助过滤一些不合理的请求

我们JavaScript中不是很容易实现保护代理,因为一般情况下我们不好判断到底是谁访问了对象

而且它不常用,但是为了让大家理解,我还是简单的举一个小例子

(不会有这样的情况,只是帮助大家理解)

function privilege(identity){
console.info('尊贵的' + identity + ',您享有全部特权~')
}


这个函数输入会员等级,会打印不同的信息

现在我写两条执行语句

demo('vip');
demo('svip');




这样我们发现不管是vip还是svip都拥有全部特权了

这显然是不公平的~(真的不是针对谁(⊙▽⊙))

所以我们加一个保护代理函数(proxy是代理的意思)

function proxyPrivilege(identity){
if(identity === 'svip'){
privilege(identity);
}else{
console.warn('氪金能使你变得更强大,用薪创造快乐,没钱xxxx');
}
}


这个保护代理函数会检测你的身份,这样只有你是svip,才可以拥有全部特权~

proxyPrivilege('svip');
proxyPrivilege('vip');




嗯这回公平了

好吧开个玩笑,这里真的没梗(才怪)

通过这个保护代理简单的例子我们来初步认识一下这个代理模式

重点是这个思想

由于它并不是我要讲的重头戏,所以在此举了一个无厘头的例子

代理的意义

看了上面的例子,可能大家要问了

为什么要使用代理啊,多麻烦啊,我一个函数解决了

function privilege(identity){
if(identity === 'svip'){
console.info('尊贵的' + identity + ',您享有全部特权~')
}else{
console.warn('氪金能使你变得更强大,用薪创造快乐,没钱xxxx');
}
}
privilege('svip');
privilege('vip');


结果相同

看起来这好像是最佳的写法,实则不是

这里我先说一个面向对象设计的原则——单一职责原则

(先简单说明一下,以后我也会整理设计原则的博文)

单一职责原则:就一个类而言,应该仅有一个引起它变化的原因

这里的类在我们JavaScript也可以引申为函数和对象

这话真的不知道是谁总结的,完全不懂什么意思啊,相当晦涩有木有

我来给大家通俗的解释一下

如果一个对象承担了太多功能,意味着这个对象就会变得庞大

这样如果其中一个功能需要改动,我们就要改变这个对象

也就是说引起它变化的原因就会有很多

所以我们最好把行为分布到颗粒化的对象中

多职责的对象导致了脆弱和低内聚的设计

解释完毕 好了回到我最开始写的代码

为什么我要这么写

假如在未来某一天所有人都可以拥有特权而不仅仅是svip(虽然不可能)

这样我要做的就是不使用保护代理(或删掉),而使用本体

但是如果你把职责都封装进一个函数,我们就需要重写这个函数

什么是单一职责?

我的代理函数负责保护,我的本体函数负责处理

这就是单一职责

由于我写的例子过于简单(并且实际没完成什么工作)

所以大家可能体会不到单一职责到底有多强大

不过没关系,往下看我们可以继续体会together

本体与代理的接口一致

上面说到了,如果某一天所有人都享有全部特权

我做的仅仅是删掉代理函数,使用本体函数

这样做的前提是什么呢?

前提是本体和代理可以相互替换

可以替换意味着具有相同的接口

不过在我所举得例子中,并没有显示出有什么接口(下面你会见到)

因为我们使用的是函数(函数是对象),函数都能够执行

再加上相同的参数,这也被认为是具有相同的接口

这就是“本体与代理的接口一致性

虚拟代理

虚拟代理是我们今天的重点(注意:重点)

虚拟代理是我们最常用的代理模式

它把一些开销很大的对象,延迟到真正需要用到这个对象的时候才去创建

下面我要举真正有意义的应用了,大家注意看

虚拟代理图片预加载

在我们浏览网站特别是网速不好的时候

我们经常可以看到本该有图片的位置,却放着占位的图片

这个图片可能是静态的,也可能是动态的

不管则样,它都代表这你要看的图片没有加载完

有了上面我们对于代理模式的理解,我也不多做解释了

我先写一个处理添加图片的本体函数

var addImg = (function(){ //加载图片本体函数
var img = document.createElement('img');
document.body.appendChild(img); //创建img节点插入文档
return function(src){
img.src = src; //设置图像src
}
})();
addImg('demo.png'); //假装从网上加载图片


这个函数负责加载图片



正常情况下不会有什么问题

但是假如你的网速特别特别卡,卡到连50KB/s都不到

你会发现图片几秒钟内不会显示,会显示空白

此时我们需要一个图片预加载的虚拟代理

功能是如果图片文件还没有下载完,先让他显示一张占位图片来表示正在加载

var addImg = (function(){
var img = document.createElement('img');
document.body.appendChild(img);
return function(src){
img.src = src;
}
})();
var proxyAddImg = (function(){ //图片预加载虚拟代理函数
var img = new Image(); //创建“灯塔”
img.onload = function(){
addImg(this.src); //一旦图片加载完毕,立刻显示
}
return function(src){
addImg('loading.gif'); //先添加“正在加载”图片
img.src = src; //预加载图片
}
})();
proxyAddImg('demo.png');


于是在你非常卡的时候,显示了一张loading的gif图片

(当然了作为占位图片,它非常小)



这里我还要做一下补充

刚才我说了,本体与代理具有接口一致性

但是这里没有体会出接口,而是以函数的形式

所以我把上面的函数修改一下,他们是完全等价的

var addImg = (function(){
var img = document.createElement('img');
document.body.appendChild(img);
return { //改
setSrc: function(src){
img.src = src;
}
}
})();
var proxyAddImg = (function(){
var img = new Image();
img.onload = function(){
addImg.setSrc(this.src); //改
}
return { //改
setSrc: function(src){
addImg.setSrc('loading.gif'); //改
img.src = src;
}
}
})();
proxyAddImg.setSrc('demo.png'); //改


这回本体与代理就是对象的形式

并且有一致的接口setSrc

虚拟代理同步数据

我们在写云笔记或者某些情况

经常会出现选中某项内容同步到服务器

但问题是这样每次点击都会向服务器发送一次请求

网络开销就会非常大

我们现在就来模拟这种情况

<input type="checkbox" id="A">内容一<br>
<input type="checkbox" id="B">内容二<br>
<input type="checkbox" id="C">内容三<br>
<input type="checkbox" id="D">内容四<br>
<input type="checkbox" id="E">内容五<br>
<input type="checkbox" id="F">内容六<br>
<input type="checkbox" id="G">内容七<br>
<input type="checkbox" id="H">内容八<br>


function synData(ID){
console.log(ID + '正在同步到服务器...');
}
var list = document.getElementsByTagName('input');
for(var i = 0, item; item = list[i]; i++){
item.onclick = function(){ //为每一项绑定点击事件
if(this.checked){ //选中状态则同步数据
synData(this.id);
}
};
}






如果你的手速非常快,在2s内可以向服务器发送8次请求

多次HTTP请求十分耗性能,

你快了,客户端和服务器谁都受不了

所以我们引入一个虚拟代理

功能是防止你的操作过于频繁,把你的请求合并到一起发送给服务器

我们设置频率是2s一次,这样的速度服务器还是可以接受的

var proxySynData = (function(){
var cache = [], //缓存我们需要同步的内容(待改进)
timer; //定时器
return function(ID){
if(!timer){ //定时器不存在就创建
timer = setTimeout(function(){
synData(cache.join()); //同步合并后的数据
cache.length = 0; //清空缓存
clearTimeout(timer); //清除定时器
timer = null; //方便垃圾回收
}, 2000);
}
cache.push(ID); //存入缓存
}
})();
var list = document.getElementsByTagName('input');
for(var i = 0, item; item = list[i]; i++){
item.onclick = function(){
if(this.checked){
proxySynData(this.id);
}
};
}


这样每过2s就会向服务器同步一次数据



但是测试过程中我还是发现了问题

我多次点击同一个会重复同步像这样



这是因为我们数组数据结构的可重复性

我们可以利用对象属性的单一性来修改代码(就是模拟集合数据结构)

(集合我过几天就写)

完整代码如下

var synData = function(ID){
console.log(ID + '正在同步到服务器...');
}
var proxySynData = (function(){
var cache = {}, //改用对象作为缓存载体
timer;
return function(ID){
if(!timer){
timer = setTimeout(function(){
synData(Object.keys(cache).join()); //改
cache = {}; //改
clearTimeout(timer);
timer = null;
}, 2000);
}
cache[ID] = 1; //我们不关注“值”,只关注“键”
}
})();
var list = document.getElementsByTagName('input');
for(var i = 0, item; item = list[i]; i++){
item.onclick = function(){
if(this.checked){
proxySynData(this.id);
}
};
}


这样的话,即使你是最强王者的手速,也最多2s同步一次数据

并且不会重复同步

虚拟代理用途广泛,但我的设计模式系列毕竟是浅析

还需要大家日后加深理解

缓存代理

缓存代理就很好理解了,可以缓存一些开销很大的运算结果

如果你第二次执行函数的时候,传递了同样的参数,那么就直接使用缓存的结果

如果运算量很大,这可是不小的优化

还是简单举个例子

写一个简单的计算加法的函数

var add = function(){
var sum = 0;
for(var i = 0, l = arguments.length; i < l; i++){
sum += arguments[i];
}
return sum;
};
console.log(add(1,2,3,4,5)); //15


很简单不多解释了

再写一个缓存代理,缓存运算结果

完整代码如下(为了证明我们使用了缓存,额外加了控制台打印信息)

var add = function(){
var sum = 0;
for(var i = 0, l = arguments.length; i < l; i++){
sum += arguments[i];
}
return sum;
};
var proxyAdd = (function(){
var cache = {}; //缓存运算结果的缓存对象
return function(){
var args = Array.prototype.join.call(arguments);//把参数用逗号组成一个字符串作为“键”
if(cache.hasOwnProperty(args)){//等价 args in cache
console.log('使用缓存结果');
return cache[args];//直接使用缓存对象的“值”
}
console.log('计算结果');
return cache[args] = add.apply(this,arguments);//使用本体函数计算结果并加入缓存
}
})();
console.log(proxyAdd(1,2,3,4,5)); //15
console.log(proxyAdd(1,2,3,4,5)); //15
console.log(proxyAdd(1,2,3,4,5)); //15




恩这样就搞定了

动态创建代理

使用高阶函数动态的创造出代理函数更加灵活

修改我们上面写过的缓存代理

我们来制造一个“工厂”

这个“工厂”用来创造代理函数

var add = function(){
var sum = 0;
for(var i = 0, l = arguments.length; i < l; i++){
sum += arguments[i];
}
return sum;
};
var proxyFnFactory = 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 proxyAdd = proxyFnFactory(add);
console.log(proxyAdd(1,2,3,4,5));
console.log(proxyAdd(1,2,3,4,5));
console.log(proxyAdd(1,2,3,4,5));


通过这个“工厂”,我们还可以制造乘法缓存代理、阶乘缓存代理…

道理是一样的,就不再赘述了

其他代理

代理模式很多,不一定每一种都适用于我们JavaScript

了解一下

防火墙代理:控制网络资源访问,很像保护代理

远程代理:为一个对象在不同地址空间提供局部代表

智能引用代理:访问对象时执行一些附加操作,比如计算对象访问次数

写时复制代理:延迟复制(庞大)对象,对象被真正修改时才复制,很像虚拟代理

总结

我们常用的代理模式就是虚拟代理(十分重要)和缓存代理

编程的时候我们不用刻意去写代理

什么时候需要用,什么时候写

不会影响你的其他代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息