javascript常见设计模式


单例模式

单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。在js中,单例作为一个命名
空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。

比如你买了个新房子,房子如果没有门,就创建一个门,如果有门就可以直接从门走出去,每个门归属于独立的房号,房间的住户可以通过门进行单例通信

模式作用:

1、模块间通信
2、系统中某个类的对象只能存在一个
3、保护自己的属性和方法

注意事项:

1、注意this的使用
2、闭包容易造成内存泄露,不需要的时候干掉
3、注意new的成本(继承)
举个栗子,页面上6个按钮的分别点击事件,如果用世界上最强的技术$.的话就是这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$('#a').click(function(){
//业务逻辑
})
$('#b').click(function(){
//业务逻辑
})
$('#c').click(function(){
//业务逻辑
})
$('#d').click(function(){
//业务逻辑
})
$('#e').click(function(){
//业务逻辑
})
$('#f').click(function(){
//业务逻辑
})

这么写的话,如果每个点击按钮的业务逻辑特别复杂,就乱掉了,而且如果$(‘#a’).click(function(){})里头声明了一个变量a=1,$(‘#b’).click(function(){})里面是不可能接收到的,因为a已经被$(‘#a’)给闭包了,所以如果用单例模式来开发这种代码,应该将它归属于不同的命名空间,比如:
a、b、c归属于tag1,d、e、f归属于tag2
采用单例的实现方式就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let tag1={
init:function(){

},
a:1,
render:function(){
let me=this
me.btna=$('#a')
},
bind:function(){
let me=this
me.bina.click(function(){
me.test()
})
},
test:function(){
a=5
}
}

let tag2={
init:function(){

},
a:1,
render:function(){
let me=this
me.btna=$('#d')
},
bind:function(){
let me=this
me.bina.click(function(){
me.test()
})
},
test:function(){
a=6
}
}

单例通讯的时候先绑定dom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let tag1={
init:function(){
this.render()
this.bind()
}
...
}

let tag2={
init:function(){
this.render()
this.bind()
},
test:function(){
tag1.a=6 //改变tag1里面的a
}
}

构造函数模式

构造函数用于创建特定类型的对象—不仅声明了使用的对象,构造函数还可以接受参数以便第一次创建对象的时候设置对象的成员值。可以自定义自己的构造函数,然后在里面声明自定义类型
对象的属性或方法。

在js中,构造函数通常是认为用来实现实例的,js没有类的概念,但是有特殊的构造函数。通过new关键字来调用自定义的构造函数,在构造函数内部,this关键字引用的是新创建的对象

模式作用:

1、用于创建特定类型的对象
2、第一次声明的时候给对象赋值
3、自己声明构造函数,赋予属性和方法

注意事项:

1、声明函数的时候处理业务逻辑
2、区分和单例的区别,配合单例实现初始化
3、构造函数大写字母开头
4、注意new的成本
代码实现其实就是构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hardy(age){
if(!(this instanceof hardy)){
return new hardy()
}
this.age=age?age:'90'
this.create=function(){
return this.age
}
}

let har=hardy(18)
let har1=hardy1(19)
console.log(har) //18
console.log(har1) //19

单例模式与构造函数结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let A={
functionhardy(age){
if(!(this instanceof hardy)){
return new hardy()
}
this.age=age?age:'90'
this.create=function(){
return this.age
}
}
}
let B={
functionhardy(age){
if(!(this instanceof hardy)){
return new hardy()
}
this.age=age?age:'90'
this.create=function(){
return this.age
}
}
}

let har=new AA.hardy(18)
let har1=new BB.hardy1(19)
console.log(har) //18
console.log(har1) //19

建造者模式

建造者模式可以将一个复杂对象的构建与其表示相分离,是的同样的构建过程可以创建不同的表示。也就是说如果我们用了建造者模式,那么用户就需要指定需要建造的类型就可以得到它们,建造者模式实际就是一个指挥者,一个建造者,一个使用指挥者调用具体建造者工作得出结果的客户。

建造者模式主要用于“分步骤构建一个复杂的对象”,在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化

比如大家再熟悉不过的$.ajax,

1
2
3
4
5
$.ajax({        //这里的$.ajax就是建造者
url:'hardy.php',
succ:function(){},
err:function(){}
})

假设:
1、产出的东西是房子
2、baogongtou调用工人进行开工,而且他要很清楚工人们的某一个大项
3、工人是盖房子的工人可以建造卧室、客厅、厨房

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function House(){
this.room=""
this.living_room=""
this.kitchen=""
}
function Foreman(){
this.gaifangzi=function(worker){
worker.make_room()
worker.make_livingroom()
worker.make_kitchen()
}
}
function Worker(){
this.make_room=function(){
console.log('卧室我建好了')
}
this.make_livingroom=function(){
console.log('客厅我建好了')
}
this.make_kitchen=function(){
console.log('厨房我建好了')
}
this.complate=function(){
var _house=new House()
_house.room="ok"
_house.living_room="ok"
_house.kitchen="ok"
return _house
}
}
let worker=new Worker()
let foreman=new Foreman()
foreman.gaifangzi(worker)
let myhouse=worker.complate()
console.log(JSON.stringify(myhouse))

工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型(抽象工厂)

这个模式十分有用,尤其是创建对象的流程赋值的时候,比如依赖于很多设置文件等。并且,你会经常在程序里看到工厂方法,用于让子类类定义需要创建的对象类型

模式作用:

1、对象的构建十分复杂
2、需要依赖具体的环境创建不同的实例
3、处理大量具有相同属性的小对象

注意事项:

不能滥用工厂,有时候仅仅是给代码增加复杂度
简单工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
let XMLHttpFactory=function(){}
XMLHttpFactory.createXMLHttp=function(){
let XMLHttp=null
if (window.XMLHttpRequest) {
XMLHttp=new XMLHttpRequest()
}else if(window.ActiveXObject){
XMLHttp=new ActiveXObject("Microsoft.XMLHTTP")
}
return XMLHttp
}
let AjaxHander=function(){
let XMLHttp=XMLHttpFactory.createXMLHttp()
}

抽象工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
XMLHttpFactory.prototype={
createFactory:function(){
throw new Error('this is an abstract class')
}
}
let XHRHander=function(){
XMLHttpFactory.call(this)
}
XHRHander.prototype=new XMLHttpFactory()
XHRHander.prototype.constructor=XHRHander

XHRHander.prototype.createFactory=function(){
var XMLHttp=null
if(window.XMLHttpRequest){
XMLHttp=new XMLHttpRequest()
}else if(window.ActiveXObject("Microsoft.XMLHTTP){
XMLHttp=new ActiveXObject('Microsoft.XMLHTTP')
}
return XMLHttp
}

外观模式

外观模式(Facade)为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式不仅简化类中的接口,而且对接口与调用者也进行了解耦。外观模式经常被认为开发者必备,它可以将一些复杂操作封装起来,并创建一个简单的接口用于调用

模式作用:

1、在设计初期,应该要意识地将不同的两个层分离,比如经典的三层结构。
2、在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观可以提供一个简单的接口,减少他们之间的依赖。
3、在维护一个遗留的大型系统时,为系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互。

注意事项:

1、外观模式被开发者连续使用时会产生一定的性能问题,因为在每次调用时都要检测功能的可用性

代码

1
2
3
4
5
6
7
let stopEvent=function(e){   //阻止冒泡和默认事件
e.stopPropagation()
e.preventDefault()
}
$('.btn').click(function(e){
stopEvent(e)
})

代理模式

代理,顾名思义就是帮助别人做事,GoF对代理模式的定义如下:
代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。
代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西

模式作用:

1、远程代理(一个对象将不同空间的对象进行局部代理)
2、虚拟代理(根据需要创建开销很大的对象如渲染网页暂时用占位符代替真图)
3、安全代理(控制真实对象的访问权限)
4、智能指引(调用对象代理处理另外一些事情如垃圾回收机制)

注意事项:

不能滥用代理,有时候仅仅是给代码增加复杂度

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function maijia(){
this.name='小明'
}
function zhongjie(){}
zhongjie.prototype.maifang=function(){
new fangdong(new maijia()).maifang('300')
}
function fangdong(maijia){
this.maijia_name=maijia.name
this.maifang=function(money){
console.log(`收到${this.maijia_name} ${money}万元`)
}
}
(new zhongjie).maifang()

观察者模式

观察者模式又叫发布订阅者模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化的时候就会通知所有的观察者对象,使得它们能够自动更新自己

模式作用:

1、支持简单的广播通信,自动通知所有已经订阅过的对象。
2、页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性
3、目标对象与观察者之间的抽象耦合关系能够扩展以及重用

注意事项:

监听要在触发之前

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//在ES5中主要是通过Object.defineProperty方法定义对象属性的设置和获取,并在进行设置时执行相关的处理函数
// 创建对象
var targetObj = {
age: 1
}
// 定义值改变时的处理函数
function observer(oldVal, newVal) {
// 其他处理逻辑...
console.info('name属性的值从 '+ oldVal +' 改变为 ' + newVal);
}

// 定义name属性及其set和get方法
Object.defineProperty(targetObj, 'name', {
enumerable: true,
configurable: true,
get: function() {
return name;
},
set: function(val) {
//调用处理函数
observer(name, val)
name = val;
}
});

targetObj.name = 'Martin';
targetObj.name = 'Lucas';
console.info('targetObj:', targetObj)

输出
name属性的值从 Martin 改变为 Lucas
targetObj: {age: 1, name: 'Lucas'}

ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TargetObj {
constructor(age, name) {
this.name = name;
this.age = age;
}
set name(val) {
observer(name, val);
name = val;
}
}

let targetObj = new TargetObj(1, 'Martin');

// 定义值改变时的处理函数
function observer(oldVal, newVal) {
// 其他处理逻辑...
console.info('name属性的值从 '+ oldVal +' 改变为 ' + newVal);
}
targetObj.name = 'Lucas';
console.info(targetObj)

输出
name属性的值从 Martin 改变为 Lucas
targetObj: {age: 1, name: 'Lucas'}

proxy Api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class TargetObj {
constructor(age, name) {
this.name = name;
this.age = age;
}
}

let targetObj = new TargetObj(1, 'Martin');

let observerProxy = new Proxy(targetObj, {
set(target, property, value, reciever) {
if (property === 'name') {
observer(target[property], value);
}

Reflect.set(target, property, value, reciever);
}
});
// 定义值改变时的处理函数
function observer(oldVal, newVal) {
// 其他处理逻辑...
console.info(`name属性的值从 ${oldVal} 改变为 ${newVal}`);
}

observerProxy.name = 'Lucas';
console.info(targetObj);

策略模式

策略模式定义了算法家族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化不会影响到使用算法的客户

模式作用:

1、所有的这些算法都是做相同的事情,只是实现不同
2、以相同的方式调用所有的方法,减少了各种算法类与使用算法类之间的耦合
3、单独定义算法类,也方便了单元测试

注意事项:

不仅可以封装算法,也可以用来封装几乎任何类型的规则,是要在分析过程中需要在不同时间应用不同的业务规则,就可以考虑是要策略模式来处理各种变化

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//比如验证一个电话号码输入
let val = document.querySelector('#input');

let valLint={
isEmpty:function(num){
return boolean
},
isTel:function(num){
return boolean
}
}//这是一个策略对象,也可以将所用的所有策略分别写入valLint里面,然后封装成一个.js文件,在需要用到策略的时候进行require然后传值判断,直接得到需要的boolean

//使用策略
let isEmpty=valLint.isEmpty(val)
let isTel=valLint.isTel(val)

if(!isEmpty&&isTel){
....
}

命令模式

命令模式(Command)的定义是:用来对方法调用进行参数化处理和传送,经过这样处理过的方法调用可以在任何需要的时候执行。也就是说该模式旨在将函数的调用、请求和操作封装成一个单一的对象,然后对这个对象进行一系列的处理。它也可以用来消除调用操作的对象和实现操作的对象之间的耦合。这为各种具体的类的更换带来了极大的灵活性。

模式作用:

1、将函数的封装、请求、调用结合为一体
2、调用具体的函数解耦命令对象与接收对象
3、提高程序模块化的灵活性

注意事项:

不需要接口一致,直接调用函数即可,以免造成浪费

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let lian={}

lian.bubing=function(bu_num){
console.log(`步兵${bu_num}开始战斗`)
}
lian.paobing=fnuction(pao_num){
console.log(`炮兵${pao_num}开始战斗`)
}
lian.lianzhang=function(mingling){
lian[mingling.type](mingling.num)
}
//发布命令
lian.lianzhang({
type:'bubing',
num:100
})
lian.lianzhang({
type:'paobing',
num:50
})
//步兵100开始战斗,炮兵50开始战斗

迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该方法中的内部表示

模式作用:

1、为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作(比如jquery中的$.each())
2、集合内部尝尝变化各异,不想暴露其内部结构,但是又想让客户代码透明地访问其中的元素,就可以用迭代器模式。

注意事项:

1、一般的迭代,至少需要有2个方法,hasNext()和Next(),这样做才做到遍历所有的对象。
2、遍历的同时更改迭代器所在的集合结构可能会导致问题(比如c#的foreach里不允许修改item)

代码:

code
code

职责链模式

职责链模式是使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连城一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
(举个栗子:客户需要开发一个系统,于是联系大boss,大boss肯定不会自己写代码,于是大boss把任务指派给项目经理,项目经理自己有几率不写,于是指派给coder来写代码,在这个过程中,客户没有直接和coder说,而是通过大boss层层指派,形成一条链,并沿着这条链传递该任务,上面说的对象指的就是我们这些coder)

链中收到请求的对象要么亲自处理它,要么转发给下一个候选者。提交方并不明确有多少个对象会处理它,任一候选者都可以响应的请求,可以在运行时刻决定哪些候选者参与到链中。

代码:

“职责链实现”
“职责链实现”

适配器模式

适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转换成客户希望的另外一个接口(方法或属性),适配器模式使得原本由
于接口不兼容而不能一起工作的那些类(对象)可以一起工作。

模式作用:

1、使用一个已经存在的对象,但其方法或接口不符合你的要求
2、创建一个可复用的对象,该对象可以与其他不相关或不可见的对象协同工作
3、使用已经存在的一个或多个对象,但是不能继承已匹配它的接口

注意事项:

与代理模式的区别,代理模式是不改变原接口适配时原接口不符合规范

举个栗子:假如本来你做的一个项目里头有一个Object

1
2
3
4
5
6
7
8
9
10
11
let a={
test:function(){},
go:function(){}
}
a.test()
//这是你原来写的东西,然后假如项目重构,别人嫌弃你的静态方法,于是写成
function b(){
this.test=function(){}
}
b.prototype.go=function(){}
//别人重构之后你所有调a的地方都要写成let a=new b(),然后改着改着自己就乱了

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//在别人重构之后,我们可以这样
function Adapter(){
let h=new b()
let a={
test:function(){
h.test()
},
go:function(){
h.go()
}
}
return a
}

let a=Adapter()
a.test()
a.go()

这样就迁移回来了,假如在参加一个比较大的项目的时候有功能一样的a.js和b.js,一般来说要判断

1
2
3
4
5
if(...){
require('a.js')
}else if(...){
require('b.js')
}

其实没必要,因为可以封装一个c.js的适配器,然后暴露a,b相应的接口对外,在a,b接口判断相应的逻辑,然后处理a.js和b.js

模板方法

模板方法(TemplateMethod)定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板方法是一种代码复用的基本技术,在类库中尤为重要,因为他们提取了类库中的公共行为。模板方法导致一种反向的控制结构,这种结构就是传说中的”好莱坞法则”,即”别找我们,我们找你”,这指的是父类调用一个类的操作,而不是相反。具体体现是面向对象编程语言里面的抽象类(以及其中的抽象方法),以及继承该抽象类(和抽象方法)的子类

模式作用:

1、一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
2、各子类中公共的行为应该被提取出来并集中到一个公共父类中规避代码重复,不同之处分离为新的操作,最后,用一个钓鱼这些新操作的模板方法来替换这些不同的代码
3、控制子类扩展,模板方法只在特定点调用”hook”,这样就允许在这些点进行扩展

注意事项:

和策略模式不同,模板方法使用继承来改变算法的一部分,而策略模式使用委托来改变整个算法

实现一个游戏流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Process(){}
Process.prototype.start=function(){
confirm('开始游戏')
}
Process.prototype.loading=function(){
console.log('loading...')
}
Process.prototype.out=function(){
confirm('结束游戏')
}

function Angry_birds(){}

Angry_birds.prototype=new Process()
Angry_birds.start()

上述代码=>建立一个游戏流程的模板,然后有一个愤怒的小鸟的游戏直接继承了该模板,有其他游戏也可以直接继承该游戏模板

原型模式

原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象,这里的prototype不是js里面的prototype,而是一个原型的英文单词,只不过js里面的prototype刚好符合这种原型模式,真正在设计模式里面是有原型模式这样的概念的,所以不能把js里面的prototype和原型模式prototype语法混淆。

对于原型模式,可以利用JavaScript特有的原型继承特性去创建对象的方式,真正的原型继承是作为最新版的ECMAScript5标准提出的,使用Object.create方法来创建这样的对象,比如Object.create(prototype,optionalDescriptorObjects)

模式作用:

原型对象本身就是有效地利用了每个构造器创建的对象

注意事项:

1、注意的依然是浅拷贝和深拷贝的问题,免得出现引用问题
2、现有的文献里查看原型模式的定义,没有针对JavaScript的,你可能发现很多讲解的都是关于类的,但是现实情况是基于原型继承 的JavaScript完全避免了类(class)的概念

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let myobj={
str:'mystring',
num:1,
myarr:[
30,
{arrgo:'i am arr'}
],
myobj1:{
innerobj:{
test:25
},
innerstr:'myobjInnerstr'
}
}

let clone=function(obj){
let obt={},k
for(k in obj){
obt[k]=obj[k]
}
return obt
}

let result=clone(myobj)
console.log(myobj)
console.log('----------------------------')
result.myobj1.innerstr=""
console.log(result)
console.log(myobj)
.log
.log

克隆之后修改了myobj1里面的innerstr,然而原来的myobj里面的myobj1[innerstr]也改变了,这是因为受到了引用的影响,可以重写一下clone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let clone=function(obj){
let obt={},k,b
if((b=(obj instanceof Array))||obj instanceof Object){
obt=b?[]:{}
for(k in obj){
if((obj[k] instanceof Array)||(obj[k] instanceof Object)){
obt[k]=clone(obj[k])
}else{
obt[k]=obj[k]
}
}
}
return obt
}

.log
.log

也可以用Object.create来实现,但是结构打印不出来,值可以拿到,这个是为什么我后续再研究一下,毕竟凌晨4点了。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let myobj={
str:'mystring',
num:1,
myarr:[
30,
{arrgo:'i am arr'}
],
obj:{
innerobj:{
test:25
},
innerstr:'myobjInnerstr'
}
}
let m=Object.create(myobj)
console.log(myobj)
console.log('---------------')
m.num=666
console.log(m.num)
console.log('---------------')
console.log(myobj)

.log
.log

!!!总之!!!!!!不要为了设计模式而设计模式!!!