注:将不变的部分和变化的部分分离是每个设计模式的主题
1.单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
核心:确保只有一个实例,并提供全局访问。
重点:惰性单例,在需要的时候才创建对象实例。
常用的设计模式,有一些对象往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。
具体案例:单击登录按钮时,页面出现一个登录浮窗,无论单击多少次,这个浮窗只会创建一次,所以这个登录浮窗适合用单例模式创建。
Js中常把全局变量当成单例使用,它满足单例的两个条件,但全局变量不是单例模式。
减少使用全局变量,因为它容易造成命名污染。
1.使用命名空间
2.使用闭包封装私有变量
通用惰性单例:
// 创建登录浮窗的案例
// 管理单例部分,用变量判断是否已经创建对象,已经创建则直接返回
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
// 创建单例部分
var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};
//调用
var createSingleLoginLayer = getSingle( createLoginLayer );
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
2.策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
目的:将算法的使用与实现分离开来。
组成:
1.策略类:封装具体的算法,并负责具体的计算过程
2.环境类:接受客户请求,将请求委托给某一个策略类
Js中函数也是对象,常把策略类和环境类直接定义为函数。
// 奖金计算案例
//策略类
var strategies = {
"S": function( salary ){
return salary * 4;
},
"A": function( salary ){
return salary * 3;
},
"B": function( salary ){
return salary * 2;
}
};
//环境类
var calculateBonus = function( level, salary ){
return strategies[ level ]( salary );
};
//调用
calculateBonus( 'S', 20000 ); // 输出:80000
calculateBonus( 'A', 10000 ); // 输出:30000
//在js语言中,策略模式是隐形的。以下也是策略模式
var S = function( salary ) {
return salary * 4;
};
var A = function( salary ) {
return salary * 3;
};
var B = function( salary ) {
return salary * 2;
};
var calculateBonus = function( func, salary ){
return func( salary );
};
calculateBonus( 'S', 10000 ) // 输出:40000
3.代理模式
定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。
关键:当客户不方便直接访问一个对象或不满足需要的时候,使用代理来控制对这个对象的访问。
类型:
1.保护代理:代理可以帮助本体过滤掉一些请求,用于控制不同权限的对象对目标对象的访问。
2.虚拟代理(最常用):把一些开销很大的对象,延迟到真正需要它的时候才去创建。
3.缓存代理:为一些开销大的运算结果提供暂时存储,下次运算时,如果参数与之前一致,则返回存储的运算结果。如计算乘积、ajax异步请求数据等。
//虚拟代理实现图片预加载的案例
//本体设置src
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( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc('http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
//虚拟代理实现合并http请求的案例
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(){ //2秒后向本体发送需要同步的ID集合
synchronousFile( cache.join( ',' ) );
clearTimeout( timer ); // 清空定时器
timer = null;
cache.length = 0; // 清空 ID 集合
}, 2000 );
}// 2 秒后向本体发送需要同步的 ID 集合
})();
//点击选中框,文件同步
var checkbox = document.getElementsByTagName( 'input' );
for ( var i = 0, c; c = checkbox[ i++ ]; ){
c.onclick = function(){
if ( this.checked === true ){
proxySynchronousFile( this.id ); }
}
};
4.迭代器模式
定义:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
类型:
1.内部迭代器:内部定义好迭代规则,外部调用一次
2.外部迭代器:必须显示请求迭代下一个元素
3.倒序迭代器:倒序访问聚合对象
4.中止迭代器:像break一样,提供一种跳出循环的方法
js语言已经内置迭代器: Array.prototype.forEach
5.发布-订阅模式
定义:又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 Js中,我们一般用事件模型来替代传统的发布—订阅模式。
作用:
1.广泛应用于异步编程中,替代传递回调函数。如订阅ajax的error、success事件等
2.可以取代对象之间硬编码的通知机制,一个对象不用再显示调用另一个对象的某个接口。
优点:时间上解耦,对象之间解耦
缺点:创建订阅者要消耗一定的时间和内存,而且订阅者会始终存在于内存中,无论订阅的消息有没有发生。
当多个发布者和订阅者嵌套在一起时,很难追踪bug产生来源。
注:不一定要先订阅后发布,也可以通过堆栈实现先发布后订阅
// 中介公司的案例
//全局事件对象
var Event = (function() {
var clientList = {}, // 缓存列表,存放订阅者的回调函数
listen, trigger, remove;
listen = function(key, fn) {
if (!clientList[key]) { //如果没订阅过此类消息,给该类消息创建一个缓存列表
clientList[key] = [];
}
clientList[key].push(fn); //订阅的消息添加进消息缓存列表
};
trigger = function() { //发布消息
var key = Array.prototype.shift.call(arguments), //取出消息类型
fns = clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) { //如果没有订阅该消息,则返回
return false;
}
for (let index = 0; index < fns.length; index++) {
const fn = fns[index];
fn.apply(this, arguments); // arguments是发布消息时附送的参数
}
};
remove = function(key, fn) {
var fns = clientList[key];
if (!fns) { // 如果key对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn) { // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
fns && (fns.length = 0);
} else {
for (var l = fn.length - 1; l >= 0 ; l--) { // 反向遍历订阅的回调函数列表
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1); // 删除订阅者的回调函数
}
}
}
};
return {
listen,
trigger,
remove
};
})();
Event.listen( 'squareMeter88', fn1 = function( price ){ // 小红订阅消息
console.log( '价格= ' + price ); // 输出:'价格=200 0000'
});
Event.trigger( 'squareMeter88', 2000000 );// 售楼处发布消息
Event.remove( 'squareMeter88', fn1 );// 删除小红的订阅
6.命令模式
命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令 。
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
撤销命令是命令模式中一个非常有用的功能,如Ctrl+Z的功能
Js中命令模式不一定要封装在 execute 方法中,也可以封装在普通函数中,和策略模式一样。
\
// 一键执行自动化的案例
var closeDoorCommand = {
execute: function(){ // 执行命令
console.log( '关门' );
},
undo: function(){ // 撤销命令
console.log( '开门' );
}
};
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
},
undo: function(){
console.log( '关电脑' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登录 QQ' );
},
undo: function(){
console.log( '退出 QQ' );
}
};
// 宏命令
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute();
}
},
undo: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.undo();
}
},
}
};
var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute(); //一键执行
macroCommand.undo(); //一键撤销
参考:
- [JavaScript设计模式与开发实践]