TOC
模式可以认为是前人对于编程问题的解决方案,而所有的设计模式的主题都是分离多变的部分和恒定的部分。
单例模式
单例模式值得就是只有一个实例的模式。单例模式的核心是确保只有一个实例,并提供全局访问。
var getSingle = function( fn ){
var result;
return function , (){
return result || ( result = fn .apply(this, arguments ) );
}
};
通用的惰性单例。利用result来存放单例,每次获取首先尝试访问result。
在合适的时候创建对象,且只创建一个。他同样能够用于很多你想只执行一次的情况,比如对某个DOM的多次事件绑定。只需你返回一个true来做验证
var bindEvent = getSingle(function(){
document.getElementById( 'div1' ).onclick = function(){
alert ( 'click' );
}
return true;
});
var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};
render();
render();
render();s
策略模式
策略模式的核心思想是:定义一系列的算法,封装这些算法,并且使他们可以互相替换
以下是一个策略模式的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 );
};
console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000
以下是利用策略模式进行表单验证的实例
/***********************策略对象**************************/
var strategies = {
isNonEmpty: function( value, errorMsg ){
if ( value === '' ){
return errorMsg;
}
},
minLength: function( value, length, errorMsg ){
if ( value.length < length ){
return errorMsg;
}
},
isMobile: function( value, errorMsg ){
if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
return errorMsg;
}
}
};
/***********************Validator 类**************************/
var Validator = function(){
this.cache = [];
};
Validator.prototype.add = function( dom, rules ){
var self = this;
for ( var i = 0, rule; rule = rules[ i++ ]; ){
(function( rule ){
var strategyAry = rule.strategy.split( ':' );
var errorMsg = rule.errorMsg;
self.cache.push(function(){
var strategy = strategyAry.shift();
strategyAry.unshift( dom.value );
strategyAry.push( errorMsg );
return strategies[ strategy ].apply( dom, strategyAry );
});
})( rule )
}
};
Validator.prototype.start = function(){
for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
var errorMsg = validatorFunc();
if ( errorMsg ){
return errorMsg;
}
}
};
/***********************客户调用代码**************************/
var registerForm = document.getElementById( 'registerForm' );
var validataFunc = function(){
var validator = new Validator();
validator.add( registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于10 位'
}]);
validator.add( registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6 位'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function(){
var errorMsg = validataFunc();
if ( errorMsg ){
alert ( errorMsg );
return false;
}
};
封装验证代码 利用Validator.add来存放代码 star来遍历执行验证 整个代码层次分明。调用方便。
代理模式
代理的主要模式有保护代理(过滤请求)、虚拟代理(预加载等)、缓存代理(缓存结果以便加速)、
通常来说,代理对于用户而言是需要关系具体实现的,代理和本体的接口应该是一致的。 7, M
虚拟代理
虚拟代理常见于各种网络请求,网络请求是一种极为耗费资源的行为,如果直接发起请求,整个过程是不顺滑的。如果你是用虚拟代理,你可以做到汇聚请求,每隔x秒后统一处理。或者是在加载图片的情况下,虚拟代理可以为你提供一张loading图,在你的真正图片请求 完毕之后,在为你加入到document中。
虚拟代理通常使用缓存数据,每当你访问虚拟代理对象,就存放入缓存数据数组之中,等待某个条件允许的时候,遍历数组,并对本体发出请求。
缓存代理
缓存代理是主要目的是缓存数据,避免所有重复的网络请求或计算。如果此结果已经存在,就直接返回。以下是一个计算乘阶的例子。
var mult = function(){
console.log( '开始计算乘积' );
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
mult( 2, 3 ); // 输出:6
mult( 2, 3, 4 ); // 输出:24
//现在加入缓存代理函数:
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
· 其中cache的key为传入的参数字符串,每当执行代理对象的时候都会事先执行判断参数是否一致。
迭代器模式
迭代器是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
迭代器分为内部迭代器和外部迭代器,他们之间的区别是内部迭代器帮你设定好了内部的迭代规则,使用不方便,但灵活性较低,外部迭代器需要显示的指定next,但灵活性较高,
在一些我们需要做兼容性检测的情况下,我们就可以使用迭代器来帮助我们获取到合适的方案,
把一些方案传入迭代器对象之中,随后遍历这个方案,如果行得通,就返回那个方案。从而让整个业务逻辑和方案可以分离开来。
发布订阅模式
发布订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖与它的对象都将得到通知,在JS中我们一般使用事件模型来替代发布-订阅模式。
其实DOM的事件模型就是一种典型的发布订阅模式
发布订阅模式在JS中的常见做法就是创建一个事件对象,包含有listen和trigger方法、当listen的时候将相应的事件名称和回调函数放入到一个聚合对象之中,当有另外一个发布者trigger的时候,就遍历所有的聚合对象的事件名称,如果符合就触发内部的回调函数。
你也可以把这个事件对象作为一个全局对象来使用,这样整个发布者和订阅者是完全不可见的,这种模式的优点在于时间和对象之间都是没有联系的。但缺点在于整个流程会变的很难定位,你很难知道这个事件到底由谁发布。
命令模式
命令模式是最简单和优雅的模式之一,命令模式中的命令值得是一个执行某些特定事情的指令。 命令模式把请求封装成一个命令对象,通过对命令对象的操作,我们可以更方便的管理请求。
var MenuBar = {
refresh: function(){
console.log( '刷新菜单界面' );
}
};
var RefreshMenuBarCommand = function( receiver ){
return {
execute: function(){
receiver.refresh();
}
}
};
var setCommand = function( button, command ){
button.onclick = function(){
command.execute();
}
};
var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );
setCommand( button1, refreshMenuBarCommand );
让我来分析一下在命令模式中的角色扮演。
- MenuBar是一个特定事件的集合对象
- RefreshMenuBarCommand是一个函数,他的目的在于把特定事件封装成一个命令对象,命令对象包含一个execute方法
- setCommand 的目的在于把按钮和命令对象的某个方法进行绑定。
组合模式
组合模式就是用最小的子对象来构建更大的对象,而这些更小的子对象本身也许是由更小的孙对象构成的。
组合模式将对象组合成树形结构,来表示整体-部分的层次结构。
组合模式有组合对象和基本对象,两者提供相同的接口,一个请求会在组合对象中向下传递到各个基本对象。比如都提供了execute接口。
组合模式不是父子关系
组合模式必须操作具有一致性,即对所有的节点都进行相同的操作
双项隐射关系,一个节点不能同时属于两个不同的组合对象。
模版方法模式
模版方法由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,由子类去具体实现。
本质上就是抽离一些子类的共同之处,抽象成一个父类来设定其基本框架。
在JS中,由于没有编译检测,最好在父类框架之中必须CreatBox方法 设定抛出异常,如果子类忘记了覆盖,就会抛出异常来警告,
并且在JS中并不需要继承来实现模版方法,完全可以让子类对象和父类对象实现一个合并。
钩子方法
钩子方法(hook)是隔离变化的一种常用手段。我们在父类容易变化的地方放置钩子,通过改变子类的钩子,我们可以改变父类封装的代码走向。
super.prototype.render = function(){
this.creatBox();
this.bindEvent();
if (this.animateHook) {
this.animate();
}
}
在这个例子中,我们从抽象父类设定的框架render中 就放置了一个钩子,我们通过修改子类对象的animateHook属性就能修改整个框架代码逻辑。
享元模式
享元模式是一种用于性能优化的模式,他能够大量的节省内存。
享元模式的重点在于分离对象的内部状态和外部状态,其中内部状态通常是存在对象内部,可以被共享,通常不会被改变的。而外部状态就是那些能被改变的部分。
通过合成内部状态和外部状态,合并成一个完整对象,从而使用他。
对象池
对象池与享元模式有些类似,但并没有分离内部外部状态。
他的核心思想是把我第一次创建的对象保存到一个数组之中,每当我需求的时候我就看对象池能否满足我的需求,如果有空余的对象就直接拿出来,没有就创建。 当销毁对象的时候把他放入到对象池中。
这种模式特别适合在网页中放入一些大量的元素的情况下,并且这些元素会历经一个快速的生命周期,他们可能经常被创建和删除。可以减少很多的开销。
责任链模式
责任链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。
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( '手机库存不足' );
}
};
Function.prototype.after = function( fn ){
var self = this;
return function(){
var ret = self.apply( this, arguments );
if ( ret === 'nextSuccessor' ){
return fn.apply( this, arguments );
}
return ret;
}
};
var order = order500yuan.after( order200yuan ).after( orderNormal );
order( 1, true, 500 ); // 输出:500 元定金预购,得到100 优惠券
order( 2, true, 500 ); // 输出:200 元定金预购,得到50 优惠券
order( 1, false, 500 ); // 输出:普通购买,无优惠券
利用AOP可以非常方便的实现责任链模式,
把后续的函数编织入主函数,然后在前置函数返回无法处理的情况下就会传递给下一个函数。
中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象就通过中介者对象来通讯,而不是相互引用,所以当一个对象发生改变,只要通知中介者对象就够了。
在中介者模式中,对象之间不知道彼此的存在。
比如在战地等游戏中,就肯定会使用中介者模式。利用中介者对象来管理,记录所有玩家的信息。
中介者模式和发布订阅模式我认为区别在于,中介者模式的目的在于管理对象的状态,而发布订阅模式的目的在于即使提醒订阅者。
装饰者模式
装饰者模式是目的在于动态的给对象增加指责。
在不同的需求之下为对象增加不同的能力。
在JS中 装饰者模式可以利用AOP方便的实现
并且AOP可以实现动态的修改参数,只要在Before中修改传入的实参就可以了(参数是基于值传递的,而引用类型会被执行同一个外层对象)
状态模式
状态模式把状态封装成类,并在把请求委托给状态对象,在不同状态之下产生不同的行为。
var delegate = function( client, delegation ){
return {
buttonWasPressed: function(){ // 将客户的操作委托给delegation 对象
return delegation.buttonWasPressed.apply( client, arguments );
}
}
};
var FSM = {
off: {
buttonWasPressed: function(){
console.log( '关灯' );
this.button.innerHTML = '下一次按我是开灯';
this.currState = this.onState;
}
},
on: {
buttonWasPressed: function(){
console.log( '开灯' );
this.button.innerHTML = '下一次按我是关灯';
this.currState = this.offState;
}
}
};
var Light = function(){
this.offState = delegate( this, FSM.off );
this.onState = delegate( this, FSM.on );
this.currState = this.offState; // 设置初始状态为关闭状态
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement( 'button' ),
self = this;
button.innerHTML = '已关灯';
this.button = document.body.appendChild( button );
this.button.onclick = function(){
self.currState.buttonWasPressed();
}
};
var light = new Light();
light.init();
利用delegate委托行为到状态机,并在具体的状态机行为中修改下一次的状态。委托的作用在这里就是重新封装创建一个函数,以便轻松的委托给状态机。
适配器模式
适配器模式可以被认为是接口不一致的弥补措施。
实质上就是对一个接口进行二次封装,从而满足我们的需求。
设计原则与技巧
主要原则如下
单一职责原则
SRP的原则体现为,一个对象,只做一件事。不要让某个对象的动机变得不存,那会让整个对象的行为变得模糊不堪,很难维护。
当然不是必须遵守,但可以被认为大多数情况下的最佳实践。
最小知识原则
指的是一个软件实体应尽量少的与其他实体发生互相作用。
比如一个对象应提供最少接口,实现对其他对象的隐藏。不然关系错综复杂,一旦维护及其难定位。
还有比如中介者模式
开放封闭原则
这是最重要的原则,
当需要改变一个程序的功能或者给这个程序增加新功能的时候,允许增加代码,但是不能修改源代码。
所以我们需要分离那些不变的部分和变化的部分,并把它们分离开来封装好
面向接口编程
也就是鸭子类型,只要你提供了合格的接口,那你就是合格的。
代码重构
重构的精髓主要有以下几方面
提炼函数
要让函数目的清晰,结构单一。
避免超大函数
合并重复的片段
如果IF语句中产生了重复的代码,就应该将他们独立出来
把条件分支语句提炼成函数
比如说一些比较复杂的IF探测,提炼成函数 提供良好的命名,看起来会更好,并且方便修改
合理使用循环
大量重复的代码可以使用循环来降低复杂度。 比如获取页面内的ajax对象。循环获取,成功遍返回。
提前让函数退出代替嵌套条件分支
挑选一个语句,成立就立马退出,避免大量的else语句
传递对象参数代替过多的参数
没有参数最好,最多两个,实在太多用对象包裹
少用三目
特别是一些大量使用的情况
合理使用链式调用
异变化的情况下使用链式可能需要让你拆分链
分解大型类
使用组合模式
使用return退出多重循环
当我使用复杂的break的时候很难判别,
最好直接return 可以将需要执行的代码组合成函数 return 目标函数 得以最后一次执行。