JavaScript 之常用设计模式

1. 单例模式

1.1. 条件

  • 确保只有一个实例
  • 可以全局访问

1.2. 适用

  • 弹出框的实现
  • 全局缓存

1.3. 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Singleton = function (name) {
this.name = name;
}
Singleton.prototype.getName = function () {
return this.name;
}
Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
let a = Singleton.getInstance('a');
let b = Singleton.getInstance('b');
console.log(a === b); // true
console.log(b.getName()); // a

因为 Javascript 是无类的语言,而 JavaScript 中的全局对象符合单例模式的两个条件,很多时候我们把全局对象当成单例模式使用:var obj ={}
下面是用闭包实现的一个单例模式:

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

var a = new CreateDiv('sven1');
var b = new CreateDiv('sven2');
alert(a === b); // true

高度抽象的单例模式代码,惰性单例的精髓:

1
2
3
4
5
6
7
// 单例模式抽象,分离创建对象的函数和判断对象是否已经创建
var getSingle = function (fn) {
var result;
return function () {
return result || ( result = fn.apply(this, arguments) );
}
};

2. 策略模式

2.1 定义

根据不同参数可以命中不同的策略

2.2. 实现

观察如下获取年终奖的 demo,根据不同的参数(level)获得不同策略方法(规则),这是策略模式在 JS 比较经典的运用之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const strategy = {
'S': function (salary) {
return salary * 4;
},
'A': function (salary) {
return salary * 3;
},
'B': function (salary) {
return salary * 2;
}
}

const calculateBonus = function (level, salary) {
return strategy[level](salary);
}

calculateBonus('A', 10000); // 30000

在函数是一等公民的 JS 中,策略模式的使用常常隐藏在高阶函数中,稍微变换下上述 demo 的形式如下,可以发现我们平时已经在使用它了,恭喜我们又掌握了一种设计模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const S = function (salary) {
return salary * 4;
}

const A = function (salary) {
return salary * 3;
}

const B = function (salary) {
return salary * 2;
}

const calculateBonus = function (func, salary) {
return func(salary);
}

calculateBonus(A, 10000); // 30000

2.3. 优点

  • 能减少大量 if 语句
  • 复用性好

3. 代理模式

3.1. 情景

情景:小明追女生 A

  • 非代理模式:小明 = 花 => 女生A
  • 代理模式:小明 = 花 => 让女生A的好友B帮忙 = 花 => 女生A

3.2. 特点

代理对象和本体对象具有一致的接口,对使用者友好。
代理模式的种类有很多,在 JS 中最常用的为虚拟代理和缓存代理。

3.3. 实现

虚拟代理实现图片预加载
下面这段代码运用代理模式来实现图片预加载,可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离,并且在未来如果不需要预加载,只要改成请求本体代替请求代理对象就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const myImage = (function() {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
const proxyImage = (function() {
const img = new Image();
img.onload = function() { // http 图片加载完毕后才会执行
myImage.setSrc(this.src);
}
return {
setSrc: function(src) {
myImage.setSrc('loading.jpg'); // 本地 loading 图片
img.src = src;
}
}
})()
proxyImage.setSrc('http://loaded.jpg');

缓存代理实现乘积计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const mult = function () {
let a = 1
for (let i = 0, l; l = arguments[i++];) {
a = a * l
}
return a
}

const proxyMult = (function () {
const cache = {}
return function () {
const tag = Array.prototype.join.call(arguments, ',')
if (cache[tag]) {
return cache[tag]
}
cache[tag] = mult.apply(this, arguments)
return cache[tag]
}
})()

proxyMult(1, 2, 3, 4) // 24

3.3. 提示

在开发时候不要先去猜测是否需要使用代理模式,如果发现直接使用某个对象不方便时,再来优化不迟。

4. 迭代器模式

4.1. 定义

能访问到聚合对象的顺序与元素

4.2. 实现

内部迭代器

1
2
3
4
5
6
7
8
9
10
function each(arr, fn) {
for (let i = 0; i < arr.length; i++) {
fn(i, arr[i]);
}
}

each([1, 2, 3], function(i, n) {
console.log(i); // 0 1 2
console.log(n); // 1 2 3
});

可以看出内部迭代器在调用的时候非常简单,使用者不用关心迭代器内部实现的细节,但这也是内部迭代器的缺点。比如要比较两数组是否相等,只能在其回调函数中作文章了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const compare = function(arr1, arr2) {
each(arr1, function(i, n) {
if (arr2[i] !== n) {
console.log('两数组不等')
return
}
})
console.log('两数组相等')
}

const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
compare(arr1, arr2) // 两数组相等

外部迭代器
相较于内部迭代器,外部迭代器将遍历的权利转移到外部,因此在调用的时候拥有了更多的自由性,不过缺点是调用方式较复杂。

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
const iterator = function (arr) {
let current = 0
const next = function () {
current = current + 1
}
const done = function () {
return current >= arr.length
}
const value = function () {
return arr[current]
}
return {
next,
done,
value,
}
}

const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
const iterator1 = iterator(arr1)
const iterator2 = iterator(arr2)

const compare = function (iterator1, iterator2) {
while (!iterator1.done() && !iterator2.done()) {
if (iterator1.value() !== iterator2.value()) {
console.log('两数组不等')
return
}
iterator1.next() // 外部迭代器将遍历的权利转移到外部
iterator2.next()
}
console.log('两数组相等')
}

compare(iterator1, iterator2)

5. 发布订阅模式

5.1. 说明

事件发布/订阅模式 (PubSub) 在异步编程中帮助我们完成更松的解耦,甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。
优点:在异步编程中实现更深的解耦
缺点:如果过多的使用发布订阅模式,会增加维护的难度

5.2. 实现

实现一个发布订阅模式

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
var Event = function () {
this.obj = {}
}

Event.prototype.on = function (eventType, fn) {
if (!this.obj[eventType]) {
this.obj[eventType] = []
}
this.obj[eventType].push(fn)
}

Event.prototype.emit = function () {
var eventType = Array.prototype.shift.call(arguments)
var arr = this.obj[eventType]
for (let i = 0; i < arr.length; i++) {
arr[i].apply(arr[i], arguments)
}
}

var ev = new Event()

ev.on('click', function (a) { // 订阅函数
console.log(a) // 1
})

ev.emit('click', 1) // 发布函数

订阅函数逻辑一定要优先于发布函数吗
考虑以下场景:

1
2
3
4
5
$.ajax('', () => {
// 异步订阅函数逻辑
})

// 在其他地方执行发布函数,此时并不能保证执行发布函数的时候,订阅函数已经执行

我们需要实现这样的逻辑:

1
2
3
4
5
6
var ev = new Event()
ev.emit('click', 1)

ev.on('click', function(a) {
console.log(a) // 1
})

目标明确后,来着手实现它:

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
var Event = function() {
this.obj = {}
this.cacheList = []
}

Event.prototype.on = function(eventType, fn) {
if (!this.obj[eventType]) {
this.obj[eventType] = []
}
this.obj[eventType].push(fn)

for (let i = 0; i < this.cacheList.length; i++) {
this.cacheList[i]()
}
}

Event.prototype.emit = function() {
const arg = arguments
const that = this
function cache() {
var eventType = Array.prototype.shift.call(arg)
var arr = that.obj[eventType]
for (let i = 0; i < arr.length; i++) {
arr[i].apply(arr[i], arg)
}
}
this.cacheList.push(cache)
}

以上代码实现思路就是把原本在 emit 里触发的函数存到 cacheList,再转交到 on 中触发。从而实现了发布函数先于订阅函数执行。

6. 命令模式

命令模式与策略模式有些类似,在 JavaScript 中它们都是隐式的。
重要性:较低

6.1. 实现

命令模式在 JavaScript 中也比较简单,下面代码中对按钮和命令进行了抽离,因此可以复杂项目中可以使用命令模式将界面的代码和功能的代码交付给不同的人去写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const setCommand = function(button, command) {
button.onClick = function() {
command.excute()
}
}

// -------------------- 上面的界面逻辑由A完成,下面的由B完成

const menu = {
updateMenu: function() {
console.log('更新菜单')
},
}

const UpdateCommand = function(receive) {
return {
excute: receive.updateMenu,
}
}

const updateCommand = UpdateCommand(menu) // 创建命令

const button1 = document.getElementById('button1')
setCommand(button1, updateCommand)

11. 中介者模式

11.1 作用

对象和对象之间借助第三方中介者进行通信。

11.2. 实现

一场测试结束后,公布结果:告知解答出题目的人挑战成功,否则挑战失败。

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
const player = function (name) {
this.name = name
playerMiddle.add(name)
}

player.prototype.win = function () {
playerMiddle.win(this.name)
}

player.prototype.lose = function () {
playerMiddle.lose(this.name)
}

const playerMiddle = (function () { // 将就用下这个 demo,这个函数当成中介者
const players = []
const winArr = []
const loseArr = []
return {
add: function (name) {
players.push(name)
},
win: function (name) {
winArr.push(name)
if (winArr.length + loseArr.length === players.length) {
this.show()
}
},
lose: function (name) {
loseArr.push(name)
if (winArr.length + loseArr.length === players.length) {
this.show()
}
},
show: function () {
for (let winner of winArr) {
console.log(winner + '挑战成功;')
}
for (let loser of loseArr) {
console.log(loser + '挑战失败;')
}
},
}
}())

const a = new player('A 选手')
const b = new player('B 选手')
const c = new player('C 选手')

a.win()
b.win()
c.lose()

// A 选手挑战成功;
// B 选手挑战成功;
// C 选手挑战失败;

在这段代码中 A、B、C 之间没有直接发生关系,而是通过另外的 playerMiddle 对象建立链接,姑且将之当成是中介者模式了。

12. 装饰者模式

12.1. 作用

动态地给函数赋能

12.2. 实现

生活中的例子:天气冷了,就添加衣服来保暖;天气热了,就将外套脱下;这个例子很形象地含盖了装饰器的神韵,随着天气的冷暖变化,衣服可以动态的穿上脱下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let wear = function () {
console.log('穿上第一件衣服')
}

const _wear1 = wear

wear = function () {
_wear1()
console.log('穿上第二件衣服')
}

const _wear2 = wear

wear = function () {
_wear2()
console.log('穿上第三件衣服')
}

wear()

// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服

这种方式有以下缺点:1:临时变量会变得越来越多;2:this 指向有时会出错
AOP 装饰函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 前置代码
Function.prototype.before = function (fn) {
const self = this
return function () {
fn.apply(this, arguments)
return self.apply(this, arguments)
}
}

// 后置代码
Function.prototype.after = function (fn) {
const self = this
return function () {
self.apply(this, arguments)
return fn.apply(this, arguments)
}
}

用后置代码来实验下上面穿衣服的 demo,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const wear1 = function () {
console.log('穿上第一件衣服')
}

const wear2 = function () {
console.log('穿上第二件衣服')
}

const wear3 = function () {
console.log('穿上第三件衣服')
}

const wear = wear1.after(wear2).after(wear3)
wear()

// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服

但这样子有时会污染原生函数,可以做点通变

1
2
3
4
5
6
7
8
9
const after = function (fn, afterFn) {
return function () {
fn.apply(this, arguments)
afterFn.apply(this, arguments)
}
}

const wear = after(after(wear1, wear2), wear3)
wear()

13. 状态模式

13.1. 定义

将事物内部的每个状态分别封装成类,内部状态改变会产生不同行为。

  • 优点:用对象代替字符串记录当前状态,状态易维护
  • 缺点:需编写大量状态类对象

13.2. 实现

某某牌电灯,按一下按钮打开弱光,按两下按钮打开强光,按三下按钮关闭灯光。
面向对象实现的状态模式

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
// 将状态封装成不同类
const weakLight = function (light) {
this.light = light
}

weakLight.prototype.press = function () {
console.log('打开强光')
this.light.setState(this.light.strongLight)
}

const strongLight = function (light) {
this.light = light
}

strongLight.prototype.press = function () {
console.log('关灯')
this.light.setState(this.light.offLight)
}

const offLight = function (light) {
this.light = light
}

offLight.prototype.press = function () {
console.log('打开弱光')
this.light.setState(this.light.weakLight)
}

const Light = function () {
this.weakLight = new weakLight(this)
this.strongLight = new strongLight(this)
this.offLight = new offLight(this)
this.currentState = this.offLight // 初始状态
}

Light.prototype.init = function () {
const btn = document.createElement('button')
btn.innerHTML = '按钮'
document.body.append(btn)
const self = this
btn.addEventListener('click', function () {
self.currentState.press()
})
}

Light.prototype.setState = function (state) { // 改变当前状态
this.currentState = state
}

const light = new Light()
light.init()

// 打开弱光
// 打开强光
// 关灯

非面向对象实现的状态模式

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
const obj = {
'weakLight': {
press: function () {
console.log('打开强光')
this.currentState = obj.strongLight
}
},
'strongLight': {
press: function () {
console.log('关灯')
this.currentState = obj.offLight
}
},
'offLight': {
press: function () {
console.log('打开弱光')
this.currentState = obj.weakLight
}
},
}

const Light = function () {
this.currentState = obj.offLight
}

Light.prototype.init = function () {
const btn = document.createElement('button')
btn.innerHTML = '按钮'
document.body.append(btn)
const self = this
btn.addEventListener('click', function () {
self.currentState.press.call(self) // 通过 call 完成委托
})
}

const light = new Light()
light.init()

14. 适配者模式

14.1. 作用

主要用于解决两个接口之间不匹配的问题。

14.2. 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const zheJiangCityOld = (function () {
return [
{
name: 'hangzhou',
id: 11,
},
{
name: 'jinhua',
id: 12,
}
]
})();

// 新接口期望的类型 { hangzhou: 11,jinhua: 12 }
const adaptor = (function () {
const obj = {};
for (let city of zheJiangCityOld) {
obj[city.name] = city.id;
}
return obj;
})();
console.log(adaptor); // { hangzhou: 11,jinhua: 12 }