模式1 – 单例模式
单例模式的核心是确保只有一个实例,并且提供全局访问。
特点:
满足“单一职责原则” : 使用代理模式,不在构造函数中判断是否已经创建过该单例;
满足惰性原则
应用:
弹出登陆窗口。
实例:
var getSingle = function (fn) {
var res;
return function() {
return res || (res = fn.apply(this, arguments));
}
}
var createPopup() {
var div = document.createElement('div');
div.innerHTML = "Login window";
div.style.display = "none";
document.body.appendChild(div);
return div;
}
var createLoginPopup = getSingle(createPopup); //create popup div here by using a given function, 满足两个原则
document.getElementById("loginBt").onclick = function() {
var popup = createLoginPopup();
pop.style.display = "block";
}
模式2 – 策略模式
定义一个个可以相互替换的算法,并且把他们封装起来。
特点:
符合开放-封闭原则 : 要修改使用的算法时不用深入函数内部进行修改,只需修改策略类;
将算法的实现与使用分离开,提高算法复用性;
通过组合、委托和多态避免多重条件选择语句;
应用:
动画实现不同的缓动效果。
一般分为两个部分:策略类于环境类。策略类用于封装各种算法,并且负责具体的计算过程; 环境类负责接收用户的请求,并且把请求委托给某一个策略类。因为各个策略类实现的算法和计算的结果不同,而环境类调用策略类的方法却是相同的,这就体现了多态性。要想实现不同的算法,只需要替换环境类中的策略类即可。
在js中,我们不必构造策略类,可直接使用函数作为策略对象。
示例:
var strategies = {
"s1": function() {
//algo 1
},
"s2": function() {
//algo 2
},
"s3": function() {
//algo 3
}
}
var someContext = new SomeConext();
someContext.start("s1"); //using s1 to calculate
//someContext.add("s1"); or add s1 as a rule for validation
模式3 – 代理模式
代理就像一个经纪人,当用户不方便直接访问某个对象或者需要对访问进行一些过滤/加工时,可以通过代理来进行对象访问。代理会对请求进行一些处理,然后再将请求传递给本体。
一般分为保护代理和虚拟代理:
保护代理负责过滤掉一些请求;
虚拟代理则是将一些花销比较大的操作延迟到真正他的时候再去创建,例如new一个对象。
特点:
保证对象符合单一职责原则;
应用:
图片预加载, 合并http请求, 惰性加载, 缓存代理(避免重复计算,可以写一个通用的缓存对象(其实就是一个闭包),将高阶函数作为参数传入)。
模式4 – 迭代器模式
顾名思义,迭代器可以将对于一个聚合对象内部元素的访问与业务逻辑分离开。
一般分为内部迭代器和外部迭代器:
内部迭代器只需一次初始调用,不需要关心迭代器的内部实现;
外部迭代器需要显式地请求下一个元素,因此可以手工控制迭代过程和顺序,例如调用iterator.next();
无论是哪种迭代器,只要聚合对象有length属性并且可以通过下标访问,那么就可以被迭代。因此类数组对象及字面量对象(用for in)都可以。
绝大部分语言都内置了迭代器。
应用:
可以通过添加终止条件来中断迭代:在callback函数中判断,如果return值为false,则通过break跳出迭代循环。
由此可以设计根据浏览器类型创建的返回对象,按优先级一个个迭代。
模式5 - 订阅发布模式
将许多对象弱耦合起来,当一个对象的状态发生变化时,所有订阅了该变化的对象都会收到通知。
DOM事件是典型的订阅发布模式,同时我们还可以自定义事件:
var event = {
clients : {},
listen : function (signal,fn){
if(!this.clients[signal]) {
this.clients[signal] = [];
}
this.clients[signal].push(fn);
},
trigger: function (arguments){ //not only trigger the event, but also send some data
var sig = Array.prototype.shift.call(arguments);
fns = this.clients[sig];
if(!fns || fns.length === 0) return false;
for(var i = 0; i < fns.length; i++) {
var fn = fns[i];
fn.apply(this, arguments);
}
},
remove: function (signal,fn){
var fns = this.clients[signal];
if(!fns) return false;
if(!fn) {
//remove all fns
delete this.clients[signal];
} else {
for(var i = 0; i <fns.length; i++) {
var _fn = fns[i];
if(_fn === fn) {
fns.splice(i,1);
}
}
}
}
}
event.listen('click',function(data){
console.log(data);
});
event.trigger('click', "Someone clicked!");
可以通过离线消息栈来保存没有被订阅的但是发生了的事件,等到有人订阅再依次取出执行。
应用:
网站登录-当用户登录成功并且ajax返回数据后,trigger事件,需要用到用户数据的渲染模块订阅该事件。
模式6-命令模式
可以解决请求发送者和请求接受者之间的耦合关系。实际上,我们只需要调用command对象中的execute方法就行,他会自动调用命令接收者对应的命令。
示例:
var tv = {
open: function() {
console.log("open tv");
},
close: function() {
console.log("turn off tv");
},
nextChannel: function() {
console.log("next channel");
}
}
//相当于我把receiver的一些可用操作封装到command对象里了,并且提供了统一的接口
var openTVCmd = function(receiver) {
return {
execute: function(){
receiver.open();
},
undo : function() {
//go to previous channel
}
}
}
var btn1 = document.getElementById("btn1");
var btn2 = document.getElementById("btn2");
var setCmd = function(button, cmd) {
button.onclick = function(){
cmd.execute();
}
}
var opentvcmd = new openTVCmd();
setCmd(btn1, opentvcmd);
btn2.onclick = function(){ //undo command
opentvcmd.undo();
}
应用:
可实现命令的撤销和重做,只需纪录一个oldState或者使用一个缓存来存放历史命令;
可实现命令队列,将command对象压入堆栈,只需依次调用他们的execute函数,由此可实现宏命令;
可分为智能命令和傻瓜命令:
1.智能命令不需要知道receiver,可自己完成请求,代码上类似策略模式,但目的不同;
2.傻瓜命令则只负责将请求传递给真正的receiver。
模式7-组合模式
组合模式将对象组合成树形结构,以表示层级结构。借助于对象的多态性,它使得用户可以统一地对待组合对象(单个对象的组合)和单个对象。
应用:
可实现宏命令,只需要调用根结点的execute,程序会自动遍历整棵树并依次执行各中间节点和叶结点的execute函数。重点是,叶结点与中间结点有统一借口。
可用来模拟文件和文件夹层级结构:
示例:
var Folder = function(name) {
this.name = name;
this.files = [];
}
Folder.prototype.add = function(file) {
this.files.push(file);
}
Folder.prototype.scan = function() {
console.log("begin scanning folder "+ this.name);
for(var i=0; i<this.files.length; i++) {
this.files[i].scan();
}
}
var File = function(name) {
this.name = name;
}
File.prototype.add = function() {
throw new Error("cannot add files to a file!");
}
File.prototype.scan = function() {
console.log("begin scanning "+this.name);
}
var folder1 = new Folder("fo1");
var folder2 = new Folder("fo2");
var file1 = new File("fi1");
var file2 = new File("fi2");
folder1.add(file1);
folder2.add(file2);
folder2.add(folder1);
注意,层级1与层级2结点之间并非父子关系,只是因为他们有统一的借口而被联系在一起。
可以对这两种结点建立双向映射,即使文件1里面含有其父结点的引用,这样子在删除一个文件时就需要将其在其父结点的files中删除。
组合模式使得用户可以忽略组合对象和单个对象的差异而统一对待,但这也会使得每个对象看上去都差不多,增加代码理解的难度。
P.s. 本文总结自《JavaScript设计模式与开发实践》,曾探著