代理模式
- 虚拟代理图片预加载
如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。
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/loading.gif');
img.src = src;
}
}
})();
proxyImage.setSrc('img/1.jpg');
- 虚拟代理合并 HTTP 请求
假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同
步到另外一台备用服务器上面。当我们选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。可以预见,如此频繁的网络请求将会带来相当大的开销。
解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统, 2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。代码如下:
var synchronousFile = function(id) {
console.log('开始同步文件, id 为: ' + id);
};
var proxySynchronousFile = (function() {
var cache = [], // 保存一段时间内需要同步的 ID
timer; // 定时器
return function(id) {
cache.push(id);
if(timer) { // 保证不会覆盖已经启动的定时器
return;
}
timer = setTimeout(function() {
synchronousFile(cache.join(',')); // 2 秒后向本体发送需要同
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);
}
}
};
责任链模式
一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象。请求发送者只需要知道链中的第一个节点。ps:可以将业务代码从if/else中释放出来,便于程序的扩展。
业务例子:公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券, 200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
/*职责链 */
/* orderType:表示订单类型(定金用户或者普通购买用户), code 的值为 1 的时候是 500 元
定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的
订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
stock:表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用
户不受此限制
*/
var order500 = function(orderType, pay, stock) {
if(orderType === 1 && pay === true) {
console.log('500 元定金预购,得到 100 优惠券');
} else {
return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
}
};
var order200 = function(orderType, pay, stock) {
if(orderType === 2 && pay === true) {
console.log('200 元定金预购,得到 50 优惠券');
} else {
return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
}
};
var orderNormal = function(orderType, pay, stock) {
if(stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
};
var Chain = function(fn) {
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor) {
return this.successor = successor;
};
Chain.prototype.passRequest = function() {
var ret = this.fn.apply(this, arguments);
if(ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
};
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
//然后指定节点在职责链中的顺序:
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
chainOrder500.passRequest( 1, true, 500 ); // 输出: 500 元定金预购,得到 100 优惠券
chainOrder500.passRequest( 2, true, 500 ); // 输出: 200 元定金预购,得到 50 优惠券
chainOrder500.passRequest( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 ); // 输出:手机库存不足
命令模式
- 撤销和重做
- 播放和录像功能
- 宏命令
中介者模式
试想一下这个场景:一个手机购买界面,有选择手机颜色,大小,型号等下拉框及输入框。每当用户进行操作后,要实时回显用户的操作,并且在某一项未选择的时候,确认按钮应该是被禁用状态。
如果把这些内容都写在操作的回调函数中,比如:在选择手机颜色onselect事件中,写很多逻辑代码。那这样就不利于程序的扩展。在以后新增了某个选项,则需要在每个输入框的回调函数中增加对此新增的下拉框的判断。
所以我们应该把逻辑代码分离出来。交由中介者去完成判断。对于新增的业务,也只需要改中介者这个对象中的业务代码。在所有输入框或下拉框的回调事件中,我们只需向中介者发个请求,告诉它,我现在变化了。中介者接收到这个信号后,会自动做一系列的处理判断。代码如下:
var goods = { // 手机库存
"red|32G": 3,
"red|16G": 0,
"blue|32G": 1,
"blue|16G": 6
};
var mediator = (function() {
var colorSelect = document.getElementById('colorSelect'),
memorySelect = document.getElementById('memorySelect'),
numberInput = document.getElementById('numberInput'),
colorInfo = document.getElementById('colorInfo'),
memoryInfo = document.getElementById('memoryInfo'),
numberInfo = document.getElementById('numberInfo'),
nextBtn = document.getElementById('nextBtn');
return {
changed: function(obj) {
var color = colorSelect.value, // 颜色
memory = memorySelect.value, // 内存
number = numberInput.value, // 数量
stock = goods[color + '|' + memory]; // 颜色和内存对应的手机库存数量
if(obj === colorSelect) { // 如果改变的是选择颜色下拉框
colorInfo.innerHTML = color;
} else if(obj === memorySelect) {
memoryInfo.innerHTML = memory;
} else if(obj === numberInput) {
numberInfo.innerHTML = number;
}
if(!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return;
}
if(!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存大小';
return;
}
if(((number - 0) | 0) !== number - 0) { // 输入购买数量是否为正整数
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购买数量';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
}
})();
// 事件函数:
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numberInput.oninput = function() {
mediator.changed(this);
};
发布-订阅模式
适用场景:一个网站,有好几个模块都需要获取用户信息后,进行渲染显示。在未用此种设计模式之前,我们会想着在获取用户信息的ajax成功回调函数中,写代码,渲染各个模块。但是这样做会导致,我们新增一个模块,需要去修改之前ajax的回调函数,加入新增部分的处理。而一个团队中,可能ajax回调函数部分是同事A做的,而新增模块是同事B负责的,其实B具体要做什么事情,A并不需要知道,A只需要告诉你,我已经拿到用户信息了,你们想干嘛就干嘛吧。那么这时候A提供出一个接口供其他需要它的人来订阅,在A完成任务后,告诉之前这些跟它打过招呼的模块,我把这些对你们有用信息传给你们,具体后续的事情由你们自己来决定。这就是发布-订阅模式。
组合模式
这种模式非常适用于树形结构
- 扫描文件夹
/******************************* Folder ******************************/
var Folder = function (name) {
this.name = name;
this.files = [];
};
Folder.prototype.add = function (file) {
this.files.push(file);
};
Folder.prototype.scan = function () {
console.log('开始扫描文件夹: ' + this.name);
for (var i = 0, file, files = this.files; file = files[i++];) {
file.scan();
}
};
/******************************* File ******************************/
var File = function (name) {
this.name = name;
};
File.prototype.add = function () {
throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function () {
console.log('开始扫描文件: ' + this.name);
};
var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var folder2 = new Folder('jQuery');
var file1 = new File('JavaScript 设计模式与开发实践');
var file2 = new File('精通 jQuery');
var file3 = new File('重构与模式')
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
var folder3 = new Folder('Nodejs');
var file4 = new File('深入浅出 Node.js');
folder3.add(file4);
var file5 = new File('JavaScript 语言精髓与编程实践');
folder.add(folder3);
folder.add(file5);
folder.scan();
装饰者模式
- 装饰方法
当我们在扩展方法的时候,更保险的方法是将原来的方法保留下来,在新的方法中再执行一次原来的方法。比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们一般都会先保存好原先的 window.onload,把它放入新的 window.onload 里执行:如:
window.onload = function() {
alert(1);
}
var _onload = window.onload || function() {};
window.onload = function() {
_onload();
alert(2);
}
但是上面会存在一些问题。1、 例如丢失了this指向。2、 需维护_onload 这个中间变量,如果函数的装饰链够长,所需的中间变量越多。
- 面向AOP编程,为方法添加前执行函数和后执行函数。
Function.prototype.before = function(beforefn) {
var __self = this; // 保存原函数的引用
return function() { // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
}
}
Function.prototype.after = function(afterfn) {
var __self = this;
return function() {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
};
网友评论