JavaScript的设计模式

综述

一般来讲,一个模式就是一个方案,可用于解决软件设计中的常见问题,在JavaScript中,模式也可以被理解为一个解决问题的模板,那些可以被广泛应用的模板。
设计模式(Design Pattern)是一种可重用的解决在编程中容易出现的一些共性问题的方案。
设计模式有如下3点好处:

  • 模式是行之有效的解决方法:它们提供固定的方案来解决code中出现的问题,是一种产时间累积下来的模式所形成的技术。
  • 模式可以很容易被重用:一个模式通常为一类问题的解决方案。这亦使得他们很健壮。
  • 模式善于表达:一般来讲,一个模式后面都会有一个或者一组帮助理解的文档或者解决方案。

在JavaScript中使用到设计模式,是为了让代码的重用性和可读性得到提升,并让代码可以更容易被维护和拓展。

模块方式

在模块方式中,是将所有的代码定义在一个对象中,然后通过对象的调用去完成功能的实现。

1
2
3
4
5
6
7
8
9
10
11
12
var Obj = {
init:function(){
// to do...
},
bind:function(data){
// to do...
var node = document.querySelector("node");
node.addEventListener("click",function(){
console.log("The data is:",data);
})
}
}

类似这样的方式将所有方法放在一个对象中,当需要使用某个功能时直接像取出对象的方式来进行调用即可完成调用。
Obj.init()Obj.bind("hello world.")
这种方式完成了代码的简单封装,但是这样的方法对象中的所有内容都被暴露在外,别人可以通过操作对象来篡改其中内容。

这时就可以用到立即执行函数,通过一个立即函数来封装代码,再将其中的接口返回出来以供使用:

1
2
3
4
5
6
7
8
9
var Func = (function(){
var foo = "hello";
var bar = "world";
return {
output:function(){
console.log(foo + " " + bar);
}
}
})()

这样的方式将函数返回出来,可以直接使用Func.output()完成调用,这时候,我们可以选用我们希望暴露的接口,而将其它内容放在其中隐藏起来,为了方便起见,可以将函数的定义放在函数体上面,在返回值上返回想要暴露的接口即可。

1
2
3
4
5
6
7
8
9
10
var Func = (function(){
var foo = "hello",
bar = "world";
function output(){
console.log(foo + " " + bar);
}
return {
output:output
}
})()

这样的方式就可以避免返回值中内容冗余。

构造器模式

JavaScript是一门基于原型的面向对象语言,而构造函数就是其中的核心,通过构造函数的方式来创建一个对象,创建完成后,通过new的方式就可以创建出许多相同的对象,这样的方式创建对象也体现了面向对象的思想:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Func(foo,bar){
this.foo = foo;
this.bar = bar;
}
Func.prototype = {
constructor:Func,
init:function(){
console.log(this.foo + " " + this.bar);
},
bind:function(){
// to do...
}
}

可以看到,以上创建构造函数的方式和定义普通函数的方式没有什么不同,只是将一些共有的方法定义在了函数的原型上,这样的写法只是为了节约内存,直接写在构造函数中也会起到一样的效果。
这时,通过new的方式去new构造函数,就可以得到构造函数的一个实例,也可以new多个,这创建的每一个实例都是相互独立的,只是他们会指向共同的prototype,本文暂且不表这部分内容,可参考之前博文JavaScript中的原型和对象,生成实例:
var instance1 = new Func(“hello”,”world”);
var instance2 = new FUnc(“你好”,”世界”);
这两个被创建出来的实例在栈内存中的地址就会指向两个不同的堆内存空间,是两个完全不同的实例。
这时,去调用instance1.init()instance2.init()是两个完全不同的结果,这也是由于二者的私有变量不同所导致的。

工厂模式

工厂模式提供一个创建一系列相互以来的对象的接口,一般而言,简单工厂模式用于对一些具有兼容性问题的内容进行判断,而无需在每次使用时都去进行判断:

1
2
3
4
5
6
7
8
9
CreateXMLHttp = function(){
var XML;
if(window.XMLHttpRequest){
XML = new XMLHttpRequest()
}else{
XML = new ActiveXObject("Microsoft.XMLHTTP");
}
return XML;
}

有了以上代码段,我们在使用ajax是就无需再去处理兼容性问题了,而可以之间调用上面定义的方法:
var Xml = CreateXMLHttp()
也可以作为构造函数的选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function factory(type){
function Bar(){
// to do...
}
Bar.prototype = {
// to do...
}
function Foo(){
// to do...
}
Foo.prototype = {
//to do...
}
// ...
if(type === "Bar"){
return new Bar();
}else if(type === "Foo"){
return new Foo()
}else{
// ...
}
}

在以上函数中,传入我们所需要的构造函数名,即可得到一个该构造函数的实例
以上被称为简单工厂模式。

在工厂模式中,还有抽象工厂模式,它先设计一个构造器,在这个构造器上去派生一些原型,最后通过对这些原型的使用来实现工厂方法,这也被称作真正的工厂模式:

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
var Func = function(){};
Func.prototype = {
some:function(){
thrownew Error("You can't use this");
},
other:function(){
this.some(); // 如果要调用这其中的方法,则会抛出一个错误
}
}
var NewFunc(){
Func.call(this); // 在当前作用域下调用上面的函数,实现继承
}
// NewFunc.prototype = new Func(); // 这句代码也可实现继承,可与上面的任选使用
function.prototype.some = function(){
// to do...
} // 对some方法进行重定义
function.prototype.say = function(){
// to do...
} // 添加一些新的方法
function.prototype.choose(type){
var foo;
switch(type){
case "a":
foo = new FuncA();
break;
case "b":
foo = new FuncB();
break;
// case ...:
// to do...
}
}

现在,去使用时,就可以这样:

1
2
var bar = new NewFunc();
var foo = bar.choose(type)

>>>more

单例模式

单一模式,顾名思义,在该模式中,它限制了一个构造函数只能有一个实例,这种模式一般是使用一个立即执行函数将一个构造器装入其中,并使用判断语句来控制实例的数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Foo = (function(){
var instance;
function Func(){
this.foo = "hello";
this.bar = "world";
}
Func.prototype.getData = function(){
console.log(this.foo + " " + this.bar);
}
if(!instance){
instance = new Func();
}
return instance;
})()

在以上程序段中可以看到,在最后被加上了一个判断,第一次调用Foo()时,他会去创建一个对象,而在后面再去调用Foo()时,由于其中的instance已经存在,所以不会再次创建实例,而是直接返回第一次时创建的对象。
这样的好处是该函数的实例永远只有一个,不过在实际使用中,应该去考虑场景才能使用这个模式,在需要多个实例的情况下,该模式显然不适用。

观察者模式

它是由一个被观察者维护着一组被称为观察者的对象,这组对象依赖于被观察者,被观察者会将自身状态的任何变化通知它们。
当特定的观察者不再需要接受来自被观察者一些状态变化时,被观察者可以从维护组中将之删除。
如果不采用这种方式,两个联系具有关联的组件必须放在一起,才能一起被执行,而采用了这种方式,则可以让两者的关联性降低。
观察者模式中一般包含如下几种组件:
被观察者:维护一组观察者,提供增加和移除观察者的方法。
观察者:提供一个接口,用于在被观察者状态被改变时,得到通知。
具体的被观察者:状态变化时广播通知观察者,并保有具体观察者的状态。
具体观察者:保持一个具体被观察者的引用,实现更新接口,用于观察,保证自身状态和被观察者相一致。

发布订阅模式

该模式和观察者模式有一定的相似性,但是也有一些值得关注的不同点:
观察者模式中,观察者必须到被观察者中去,而在JavaScript中,发布订阅事件则是使用自定义监听事件的方式来完成其中的广播和通知功能,这种方式更大程度上的避免了二者完全解耦。
在JavaScript中,有许多的事件,一个最简单的事件的例子:

1
2
3
node.onclick = function(){
console.log(1);
}

在以上代码片段中,当去点击node节点时,控制台会打印出1,这就相当于后面这个函数订阅了node发布的click事件,当这个事件被触发,在控制台打印1,后面的匿名函数和前面节点的click事件就是一个发布订阅的关系,而在其它情况下,让一个内容被执行后给出一个自定义的事件,当其它需要一个在前一个内容被执行完成后才会执行的函数就去监听这个自定义事件,即是一个发布订阅模式。
到这里,我们的问题就变成了如何添加、触发和移除自定义事件:
可以使用构造函数Event来创建一个新的事件对象:

1
2
3
4
5
6
7
8
9
10
11
var event = new Event("hello");
var body = document.querySelector("body");
body.innerText = "hello";
setInterval(function(){
if(body.innerText === "hello"){
body.dispatchEvent(event)
}})
body.addEventListener("hello",function(){
console.log("hello");
body.innerText = "world";
})

以上代码段中是一个最简单创建和调用自定义事件的方法,通过这样的方式,就有了一个自定义事件hello,它和原生事件的调用并没有什么不同之处。
也可以通过自定义的方式来做一个事件管理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var EventManager = (function(){
var event = {};
function on(type,func){
event[type] = event[type] || [];
event[type].push({
"handler":handler
})
}
function bind(type,args){
if(!event[type]) return;
for(var i = 0;i < event[type].length;i++){
event[type][i].handler(args);
}
}
function off(type){
delete(event[type]);
}
return {
on:on,
bind:bind,
off:off
}
})()

以上代码段中,是用了一个立即执行函数来将所有的事件放入其中,再将其赋给一个变量, 通过操作返回值来增删和触发自定义事件

1
2
3
4
5
EventManager.on("change",function(data){
console.log("something is change.",data);
}) // 增加一个叫做change的事件和被触发时需要做的事情
EventManager.bind("change","it's me"); // 触发事件,并传入参数
EventManager.off("change") // 删除change事件

通过以上方式,就可以实现自定义事件的增删、调用了,也可以修改上述代码,将出发的时间放到bind中传入,方法相同,就不表了。