对《js设计模式与开发实践》一书的总结,感谢作者,以下是笔者自己码的字,挺辛苦的,给个赞吧,转载,请添加源链接
一、常用设计模式
1、单例模式:确保只有一个实例,并提供全局访问。
2、策略模式:定义一些列的算法,把它们一个个封装起来,并且使它们可以相互替换。
3、代理模式:为一个对象提供一个代用品或占位符,以便控制对它的访问。js里虚拟代理(网络请求方面)、缓存代理(数据方面)最常用
4、迭代器模式:提供一种方法,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。不需要关心对象的内部构造,也可以按顺序访问其中的每个元素。很多语言都有自己内置的迭代器,比如js的Array.prototype.forEach
5、发布-订阅模式:又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态生改变时,所有依赖于它的对象都将得到通知。在js中,一般用事件模型代替它。
PS:
var a={}
a.b={}
a.c=function(key,fn){
if(!this.b[key]){
this.b[key]=[]
}
this.b[key].push(fn)
}
a.d=function(){
var key =Array.prototype.shift.call(arguments),
fns=this.b[key]
console.log(arguments)
if(!fns||fns.length===0){
return false;
}
for(var i=0,fn;fn=fns[i++];){
fn.apply(this,arguments)
}
}
a.c('88',function(pri){
console.log(pri)
})
a.c('110',function(pri){
console.log(pri)
})
a.d('88',200)
a.d('110',300)
6、命令模式:执行某些特定事情的指令。记录信息的清单,就是命令模式中的命令对象。
PS:有时候,需要像某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。这时候就需要命令模式,使得请求发送者和请求接收者能够消除耦合关系。
7、组合模式:使用树形方式创建对象的结构,把相同的操作应用在组合对象和单个对象上。
8、模板方法模式:只需要集成就可以实现,由两部分组成,第一部分是抽象类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
PS:
//抽象类
var a=function(){};
a.prototype.b=function(){
console.log('把水煮沸')
}
a.prototype.c=function(){}
a.prototype.d=function(){}
a.prototype.e=function(){}
a.prototype.init=function(){
this.b();
this.c();
this.d();
this.e();
}
//子类
var g=function(){}
g.prototype.c=function(){
console.log('用沸水冲咖啡')
}
g.prototype.d=function(){
console.log('把咖啡倒进杯子')
}
g.prototype.e=function(){
console.log('加糖和牛奶')
}
var h=new g()
h.init()
这里的a.prototype.init就是模板方法
9、好莱坞原则:即高层组件调用底层组件,模板方法是好莱坞的一个典型使用场景;子类放弃了对自己的控制权,而是改为父类通知子类哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。
Ps:发布-订阅模式、回调函数都用到了此原则
10、享元模式:运用共享技术来有效支持大量细粒度的对象。
Ps:
- 内部状态存储与对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
11、对象池:维护一个装在空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责后,再进入池子等待被下次获取;
12、职责链模式:使多个对象都有机会处理请求,并从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
PS:有多个if-else条件的时候,要去考虑是否可以用职责链模式。
13、中介者模式:解除对象与对象之间得耦合关系。增加一个中介者对象后,所有得相关对象都通过中介者对象来通信,而不是互相引用,所有当一个对象发生改变时,只需要通知中介者对象即可。此模式迎合迪米特法则的一种实现,迪米特也叫做最少知识原则,是指一个对象应该尽可能少地了解另外的对象。
PS:编写思路:1、利用发布-订阅模式2、在中介者对象中开放一些接收消息得接口
14、装饰者模式:给对象动态地增加职责。这种方式并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。
PS:经常用到Function.pototype.after和Function.pototype.before两个函数进行装饰
Function.prototype.before=function(beforefn){
var _self=this;//保存原函数引用
return function(){// 返回包含了原函数和新函数的“代理”函数
beforefn.apply(this,arguments);// 执行新函数,且保证this不被劫持,新函数接受的参数也会被原封不动地传入原函数,新函数在原函数之前执行
return _self.apply(this,arguments);// 执行原函数并返回原函数的执行结果,并且保证this不被劫持
}
}
注意:这里的beforefn和原函数_self共用一组参数列表arguments,当我们在beforefn的函数体内改变arguments的时候,_self接收的参数列表自然也会变化,所以经常用于动态地改变原函数的参数
15、状态模式:关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。此模式关键是把事物的每种状态都封装成单独的类。
PS:状态模式和策略模式区别:
策略模式的各个策略类之间是平等又平行的,他们之间没有任何联系,所以客户必须熟知这些策略类的作用。以便随时切换算法;而状态模式,状态和状态对应的行为早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事发生在状态模式内部。对客户来说,并不需要了解这些细节,这正是状态模式的作用所在。
16、适配器模式:解决两个软件实体的接口不兼容的问题。不考虑接口是怎样实现的,也不考虑将来可能会如何让变化,适配器模式不需要改变已有的接口,就能够使他们协同作用,适配器模式通常只包装一次。
PS:
var a={
show:function(){
console.log('a数据源开始渲染')
}
}
var b={
disply:function(){
console.log('b数据源开始渲染')
}
}
//适配器c
var c={
show:function(){
return b.disply();
}
}
var render=function(fn){
fn.show()
}
render(a)
render(c)
17、外观模式:在JS中不常用。主要是为子系统中的一组接口提供一个一致的界面,定义了一个高层的接口,这个接口使子系统更加容易使用。在C++或者JAVA中指的是一组类的集合,这些类相互协作可以组成系统中一个相对独立的部分。但在JS中,这个子系统至少应该指的是一组函数的集合。
PS:
var A=function(){
a1()
a2()
}
二、番外篇:
1、单一职责原则(SRP):一个对象或方法只做一件事。代理、单例、迭代、装饰模式经常用到。
PS:
- 如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如ajax,创建xhr和发送xhr总是在一起,那么这个过程就没必要分开
- 职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但他们还没有发生改变的征兆,那么也许没有必要主动分离它们,重构的时候在进行分离也不迟。
2、最少知识原则(LKP):也叫“迪米特法则”,一个软件实体应当尽可能少地与其他实体发生相互作用。软件实体指的是系统、类、模块、函数、变量、对象等等。
PS:
- 尽量减少对象之间的交互。
- 非要联系的化,可使用第三者对象承担两者之间的通信。
- 也可以让对象暴露必要的接口,让对象之间的联系限制在最小范围内。
- 闭包,很适合此原则。
此原则,也要根据实际开发中,情况而定,不能一味的遵守最少原则,不然会不断的增加第三对象,庞大到难以维护。
3、开放-封闭原则:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
PS:
- 最明显的就是找出程序中将要发生变化地地方,然后把变化封装起来。
- 通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统中,只需替换容易变化的部分,如果这些部分被封装好了,替换也就容易多了(可以指上面例子的b\c\d)。而变化的部分之外(不变的,可以指上面例子的a)就是稳定的部分,稳定的部分是不需要改变的。
- Hook、回调函数都可以帮助解决开闭原则
- 回调函数,把部分容易变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。这是高阶函数的技法
- 挑选出最容易发生变化的地方,然后构造抽象来封闭这些变化。
- 在不可避免发生修改的时候,尽量修改哪些相对容易的地方。比如,修改某个开源库的配置文件,总比修改它的源码简单的多。
- 把if换成switch-case是没用的,换汤不换药。看到if和switch的时候,要考虑是否可以用对象的多态性来重构它们。把不变的部分隔离出来,把可变的部分封装起来。
-
var a=function(m){ m.sound() } var b=function(){} b.prototype.sound=function(){ console.log('bbbb') } var c=function(){} c.prototype.sound=function(){ console.log('cccc') } a(new b()) a(new c()) //增加一个跟b和c一样的行为对象d,不用该a函数 var d=function(){} d.prototype.sound=function(){ console.log('dddd') } a(new d())
三、写代码注意点:
- 在最初编写代码的时候,先假设变化永远不会发生,这有利于我们速度完成需求。当变化发生并且对我们接下来的工作造成影响的时候,可以在回过头来封装这些变化的地方。然后确保我们不会掉进同一个坑里。
- 开闭原则是编写一个好程序的目标,其他设计原则和设计模式都是达到这个目标的过程。
- 将不变的部分和变化的部分隔开是每个设计模式的主题
- 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。