什么是设计模式?

通俗的说,设计模式就是开发人员在时间中面临的一般问题的解决方案,这些解决方案是众多开发人员经过相当长的一段时间的试验和错误总结出来的。最初由GOF(Gang of Four)在设计模式一书中归纳并提到设计模式的概念。实际上,这些解决方案我们开发中可能会不经意中使用到类似的代码或逻辑,但并不知道这类解决方案还有一个具体的名字。这就是设计模式这个概念的意义,将一些解决方案规范化、通用化,以便再次使用。

类型

《设计模式》一书中提到的设计模式总共有23中(并不是只有23种设计模式、只是提出23中常用的),这些模式可分为三大类:创建型模式、结构性模式、行为型模式。

  • 创建型模式:这些设计模式提供了一种在创建对象的同时隐藏逻辑的方式,而不是使用new运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。(如原型模式)
  • 结构性模式:这些设计模式关注类和对象的组合。继承的概念被用来组合
  • 行为型模式:这些设计模式特别关注对象之间的通信

设计模式的六大原则

  1. 开闭原则:对扩展开放,对修改关闭。
  2. 里氏代换原则:任何基类可以出现的地方,子类一定可以出现
  3. 依赖倒转原则:针对接口编程,依赖于抽象而不依赖于具体
  4. 接口隔离原则:使用多个隔离的接口,比使用单个接口要好。即降低类之间的耦合度
  5. 迪米特法则:又称最少知道原则,一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相互独立
  6. 合成复用原则:尽量使用合成/聚合的方式,而不是使用继承

单例模式

单例模式时一种较为简单的创建型模式。当涉及到只需要使用某个类的一个实例对象时,这种模式就很有帮助(如网站中的登录框,往往在多次弹窗下呈现的都是同一个登录框)
单例模式的核心意图就是保证一个类只有一个实例,并提供一个可以访问它的全局访问点。

单例模式实现

  • 因为要保证只有一个实例被创建,所以需要保证构造函数不被暴露,即为私有的构造函数。
  • 同时需要判断实例是否已经创建,若已创建,则返回实例,未创建则创建后保存

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let SingleObject = function(name){
this.name = name;
//instance记录已经创建的实例
this.instance = null;
}
SingleObject.prototype.getName = function(){
console.log(this.name)
}
//使用getInstance方法来获取唯一实例
SingleObject.getInstance = function(name){
if(!this.instance){
this.instance = new SingleObject(name);
}
return this.instance
}

let a = SingleObject.getInstance();
let b = SingleObject.getInstance();
console.log(a===b) //true

上述方式可以实现,通过getInstance方法来获取唯一的实例,但是此时的SingleObject是向外暴露的,仍然可以通过new来创建实例,不符合我们想要的单例。

创建透明的单例模式

即用户在创建对象实例时,可以向普通类一样进行创建,而返回的实例对象时唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let SingleObject = (function(){
//保存单例
let instance;
//对外暴露的构造函数
let singleObject = function(name){
if(instance)return instance;
this.name = name;
return instance = this;
}
singleObject.prototype.getName = function(){
return this.name;
}

return singleObject;
})()

let a = new SingleObject("hello");
let b = new SingleObject("world");
console.log(b.getName(),a===b) //hello true

这里是通过闭包的方式实现instance属性的私有化,并在构造函数中返回实例对象(这样通过new得到的对象就是返回的最初创建的实例instance而不是每次new的新this)。

上述方式已经实现了一个简单的单例模式,但代码的复用性不高,以创建一个登录框为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let CreateLogin = (function(){
let instance;
let createLogin = function(html){
if(instance)return instance;
this.html = html;
this.init();
return instance = this;
}
createLogin.prototype.init = function(){
this.div = document.createElement('div');
this.div.innerHTML = this.html;
document.body.appendChild(this.div);
}
return createLogin;
})()

let loginDom = new CreateLogin("Login")
let loginCopy = new CreateLogin("l2"); //在程序的其他地方也可以使用此步骤来获取登录框实例从而进行操作
console.log(loginDom === loginCopy); //true

仔细观察可以发现,对实例的管理逻辑与对登录业务的管理逻辑都写在一起,此时如果要实现一个类似的公告弹窗,则需要对代码进行大量修改。
所以需要将单例模式的实例管理逻辑与业务代码相分离,才可实现可复用的代码。如下

抽离并优化代码

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
//创建登录框的逻辑
let CreateLogin = function(){
this.div = document.createElement('div');
this.div.innerHTML = "这是一个登录窗口";
div.style.display = "none";
document.body.appendChild(this.div);
return div;
}

//用于单例包装
let getSingle = function(fn){
let result;
return function(){
return result || (result = fn.apply(this, arguments));
}
}

//将上述CreateLogin包装为单例模式
let createSingleLogin = getSingle(CreateLogin);

//登录绑定,假设有一个button
button.onclick = function(){
let login = createSingleLogin();
login.style.display = 'block'
}

//同理,只要再有一个构造函数CreateXXX,也可以公国getSingle方法将其包装为一个单例模式。

拿右键菜单做个实战

之前有对博客做过一个右键菜单的教程,这里可以考虑对其进行优化。很明显,整个页面只会有一个右键菜单,这时就可以将它设计成一个单例模式。完整见右键菜单(可能还没更新,因为写的还不完善)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
let RightMenu = (function(){
// rm保存右键菜单dom实例
let rm;
//data保存菜单选项
let data = {};
//dataDom保存子菜单dom实例
let dataDom = {};
//渲染防抖
let renderTimer;

return {
getInstance: function(){
//如果页面中已经有右键菜单实例则直接返回此实例
if(rm)return rm;
//页面中没有右键菜单实例则创建
rm = document.createElement('div');
rm.id = 'rightMenu';
document.body.appendChild(rm);
return rm;
},
setItem(group, ...items){
//添加菜单项
if(!data[group])data[group] = [];
data[group].push(...items);

this.render();
},
render(){
if(renderTimer){
clearTimeout(renderTimer);
}
renderTimer = setTimeout(()=>{
let rm = this.getInstance();
//将数据映射到视图中...
},200)
},
show(options){
//显示右键菜单...
},
hide(){
//隐藏右键菜单...
}
}
})();


...




//使用时,可通过getInstance函数来获取唯一实例
RightMenu.getInstance();
//调用方法操作实例
RightMenu.setItem('s-top', menu1, menu2);
RightMenu.setItem('normal', menu3, menu4);

//触发事件时 显示/隐藏 右键菜单
window.addEventListener('click',()=>{RightMenu.hide()})
window.addEventListener('contextmenu',()=>{
if(event.ctrlKey){
return true;
}
if(kk.selectText = document.getSelection().toString()){
RightMenu.show({
x:event.pageX,
y:event.pageY,
groups:['s-top','text','normal']
});
}else{
RightMenu.show({
x:event.pageX,
y:event.pageY,
groups:['s-top','normal']
});
}
event.stopPropagation();
event.preventDefault();
return false;
})

单例模式小结

  • 单例模式的核心是确保一个类只有一个实例,并提供它的全局访问点
  • 对于在需要的时候才创建实例(比如使用全局变量模拟的单例模式var a_instance = {name:"name"}就是在最初就创建好的,而上个例子中的右键菜单就是在第一次调用getInstance时才会创建)的单例可以成为惰性单例(比如有时用户只是浏览一下网站,而没有登录的意愿,此时登录框就可以不创建了)

    后记

    之前找实习面试的时候被狠狠地问了一波设计模式,当时啥都不懂,乱答一通,只好趁着暑假找补找补了🤪
    js里面关于单例模式实际上只需要定义一个全局对象就可以实现基本功能,所以可以将全局变量来当做单例模式使用,但是会造成命名空间的污染,项目过大的话变量还容易被覆盖。所以为了减少全局变量带来的污染,可以使用命名空间(最简单的就是使用对象字面量的方式var namespace1={a:function(){console.log(1)}})或者使用闭包的方式(如上述实现单例模式)。