JavaScript 设计模式与开发实践
# 1. 高阶函数实现 AOP
Function.prototype.before = function(beforefn) {
var __self = this // 保存原函数的引用
// 返回包含了原函数和新函数的"代理"函数
return function() {
beforefn.apply(this, arguments) // 执行新函数,修正 this
return __self.apply(this, arguments) // 执行原函数
}
}
Function.prototype.after = function(afterfn) {
var __self = this
return function() {
var ret = __self.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
var func = function() {
console.log(2)
}
func = func
.before(function() {
console.log(1)
})
.after(function() {
console.log(3)
})
func()
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
# 2. 高阶函数的其他应用
- currying 函数柯里化(柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程)。
var currying = function(fn) {
var args = []
return function() {
if (arguments.length === 0) {
return fn.apply(this, args)
} else {
;[].push.apply(args, arguments)
return arguments.callee
}
}
}
var cost = (function() {
var money = 0
return function() {
for (var i = 0, l = arguments.length; i < l; i++) {
money += arguments[i]
}
return money
}
})()
var cost = currying(cost) // 转化成 currying 函数
cost(100) // 未真正求值
cost(200) // 未真正求值
cost(300) // 未真正求值
alert(cost()) // 求值并输出:600
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- uncurrying 反柯里化(反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。目的是创建一个更普适性的函数,可以被不同的对象使用。有鸠占鹊巢的效果)。
// JavaScript 之父 Brendan Eich
Function.prototype.uncurrying = function() {
var self = this
return function() {
var obj = Array.prototype.shift.call(arguments)
return self.apply(obj, arguments)
}
}
// 另一种实现方式
Function.prototype.uncurrying = function() {
var self = this
return function() {
return Function.prototype.call.apply(self, arguments)
}
}
for (var i = 0, fn, ary = ['push', 'shift', 'forEach']; (fn = ary[i++]); ) {
Array[fn] = Array.prototype[fn].uncurrying()
}
var obj = {
length: 3,
'0': 1,
'1': 2,
'2': 3,
}
Array.push(obj, 4) // 向对象中添加一个元素
console.log(obj.length) // 输出:4
var first = Array.shift(obj) // 截取第一个元素
console.log(first) // 输出:1
console.log(obj) // 输出:{0: 2, 1: 3, 2: 4, length: 3}
Array.forEach(obj, function(i, n) {
console.log(n) // 分别输出:0, 1, 2
})
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
# 3. 单例模式
- 单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
// 示例一
var Singleton = function(name) {
this.name = name
this.instance = null
}
Singleton.prototype.getName = function() {
alert(this.name)
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')
alert(a === b) // true
// 示例二(增加不透明性)
var Singleton = function(name) {
this.name = name
}
Singleton.prototype.getName = function() {
alert(this.name)
}
Singleton.getInstance = (function() {
var instance = null
return function(name) {
if (!instance) {
instance = new Singleton(name)
}
return instance
}
})()
var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')
alert(a === b) // true
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
# 4. 惰性单例
- 结合闭包,惰性创建唯一节点。
<body>
<button id="loginBtn">登录</button>
</body>
2
3
var createLoginLayer = (function() {
var div
return function() {
if (!div) {
div = document.createElement('div')
div.innerHTML = '我是登录浮窗'
div.style.display = 'none'
document.body.appendChild(div)
}
return div
}
})()
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5. 通用的惰性单例
上一节我们完成了一个可用的惰性单例,但是我们发现它还有如下一些问题。
- 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在 createLoginLayer 对象内部。
- 如果我们下次需要创建页面中唯一的 iframe ,或者 script 标签,用来跨域请求数据,就必须得如法炮制,把 createLoginLayer 函数几乎照抄一遍。
通用的惰性单例。
var getSingle = function(fn) {
var result
return function() {
return result || (result = fn.apply(this, arguments))
}
}
var createLoginLayer = function() {
var div = document.createElement('div')
div.innerHTML = '我是登录浮窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
var createSingleLoginLayer = getSingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 6. 策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
“并且使它们可以相互替换”,这句话在很大程度上是相对于静态类型语言而言的。因为静态类型语言中有类型检查机制,所以各个策略类需要实现同样的接口。当它们的真正类型被隐藏在接口后面时,它们才能被相互替换。而在 JavaScript 这种 “类型模糊” 的语言中没有这种困扰,任何对象都可以被替换使用。因此,JavaScript 中的 “可以相互替换使用” 表现为它们具有相同的目标和意图。
一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context 中要维持对某个策略对象的引用。
// 策略类
var performanceS = function() {}
performanceS.prototype.calculate = function(salary) {
return salary * 4
}
var performanceA = function() {}
performanceA.prototype.calculate = function(salary) {
return salary * 3
}
var performanceB = function() {}
performanceB.prototype.calculate = function(salary) {
return salary * 2
}
// 环境类
var Bonus = function() {
this.salary = null // 原始工资
this.strategy = null // 绩效等级对应的策略对象
}
Bonus.prototype.setSalary = function(salary) {
this.salary = salary // 设置员工的原始工资
}
Bonus.prototype.setStrategy = function(strategy) {
this.strategy = strategy // 设置员工绩效等级对应的策略对象
}
Bonus.prototype.getBonus = function() {
// 取得奖金数额
return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对应的策略对象
}
var bonus = new Bonus()
bonus.setSalary(10000)
bonus.setStrategy(new performanceS()) // 设置策略对象
console.log(bonus.getBonus()) // 输出:40000
bonus.setStrategy(new performanceA()) // 设置策略对象
console.log(bonus.getBonus()) // 输出:30000
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
# 7. JavaScript 版本的策略模式
var strategies = {
S: function(salary) {
return salary * 4
},
A: function(salary) {
return salary * 3
},
B: function(salary) {
return salary * 2
},
}
var calculateBonus = function(level, salary) {
return strategies[level](salary)
}
console.log(calculateBonus('S', 20000)) // 输出:80000
console.log(calculateBonus('A', 10000)) // 输出:30000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 8. 策略模式的优缺点
优点。
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
当然,策略模式也有一些缺点,但这些缺点并不严重。
# 9. 一等函数对象与策略模式
- Peter Norvig 在他的演讲中曾说过:“在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量。” 在 JavaScript 中,除了使用类来封装算法和行为之外,使用函数当然也是一种选择。这些 “算法” 可以被封装到函数中并且四处传递,也就是我们常说的 “高阶函数”。实际上在 JavaScript 这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。当我们对这些函数发出 “调用” 的消息时,不同的函数会返回不同的执行结果。在 JavaScript 中,“函数对象的多态性” 来得更加简单。
# 10. 代理模式
- 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
// 小明让B代理对女神(A)送花
var Flower = function() {}
var xiaoming = {
sendFlower: function(target) {
var flower = new Flower()
target.receiveFlower(flower)
},
}
var B = {
receiveFlower: function(flower) {
A.receiveFlower(flower)
},
}
var A = {
receiveFlower: function(flower) {
console.log('收到花 ' + flower)
},
}
xiaoming.sendFlower(B)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 11. 保护代理和虚拟代理
虽然这只是个虚拟的例子,但我们可以从中找到两种代理模式的身影。代理 B 可以帮助 A 过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B 处被拒绝掉。这种代理叫作保护代理。A 和 B 一个充当白脸,一个充当黑脸。白脸 A 继续保持良好的女神形象,不希望直接拒绝任何人,于是找了黑脸 B 来控制对 A 的访问。
另外,假设现实中的花价格不菲,导致在程序世界里,new Flower 也是一个代价昂贵的操作,那么我们可以把 new Flower 的操作交给代理 B 去执行,代理 B 会选择在 A 心情好时再执行 new Flower,这是代理模式的另一种形式,叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
保护代理用于控制不同权限的对象对目标对象的访问,但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。
# 12. 代理的意义
- 单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。
# 13. 缓存代理
- 计算乘积。
var proxyMult = (function() {
var cache = {}
return function() {
var args = Array.prototype.join.call(arguments, ',')
if (args in cache) {
return cache[args]
}
return (cache[args] = mult.apply(this, arguments))
}
})()
proxyMult(1, 2, 3, 4) // 输出:24
proxyMult(1, 2, 3, 4) // 输出:24
2
3
4
5
6
7
8
9
10
11
12
# 14. 迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。目前,恐怕只有在一些 “古董级” 的语言中才会为实现一个迭代器模式而烦恼。
迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前的绝大部分语言都内置了迭代器。
# 15. 发布 — 订阅模式
发布 — 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
- 可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅 ajax 请求的 error 、 succ 等事件。
- 可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。
小结。
- 优点:一为时间上的解耦,二为对象之间的解耦。
- 缺点:创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布 — 订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个 bug 不是件轻松的事情。
# 16. 全局的发布 - 订阅对象
var Event = (function() {
var clientList = {},
listen,
trigger,
remove
listen = function(key, fn) {
if (!clientList[key]) {
clientList[key] = []
}
clientList[key].push(fn)
}
trigger = function() {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key]
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments)
}
}
remove = function(key, fn) {
var fns = clientList[key]
if (!fns) {
return false
}
if (!fn) {
fns && (fns.length = 0)
} else {
for (var l = fns.length - 1; l >= 0; l--) {
var _fn = fns[l]
if (_fn === fn) {
fns.splice(l, 1)
}
}
}
}
return {
listen: listen,
trigger: trigger,
remove: remove,
}
})()
Event.listen('squareMeter88', function(price) {
// 小红订阅消息
console.log('价格= ' + price) // 输出:'价格=2000000'
})
Event.trigger('squareMeter88', 2000000) // 售楼处发布消息
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
# 17. 必须先订阅再发布吗
我们所了解到的发布 — 订阅模式,都是订阅者必须先订阅一个消息,随后才能接收到发布者发布的消息。如果把顺序反过来,发布者先发布一条消息,而在此之前并没有对象来订阅它,这条消息无疑将消失在宇宙中。
在某些情况下,我们需要先将这条消息保存下来,等到有对象来订阅它的时候,再重新把消息发布给订阅者。就如同 QQ 中的离线消息一样,离线消息被保存在服务器中,接收人下次登录上线之后,可以重新收到这条消息。
# 18. JavaScript 实现发布 - 订阅模式的便利性
- 在 Java 中实现一个自己的发布 — 订阅模式,通常会把订阅者对象自身当成引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如 update 的方法,供发布者对象在适合的时候调用。而在 JavaScript 中,我们用注册回调函数的形式来代替传统的发布 — 订阅模式,显得更加优雅和简单。
# 19. 命令模式
命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
小结:跟许多其他语言不同,JavaScript 可以用高阶函数非常方便地实现命令模式。命令模式在 JavaScript 语言中是一种隐形的模式。
# 20. JavaScript 中的命令模式
命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。
JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。
var bindClick = function(button, func) {
button.onclick = func
}
var MenuBar = {
refresh: function() {
console.log('刷新菜单界面')
},
}
var SubMenu = {
add: function() {
console.log('增加子菜单')
},
del: function() {
console.log('删除子菜单')
},
}
bindClick(button1, MenuBar.refresh)
bindClick(button2, SubMenu.add)
bindClick(button3, SubMenu.del)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 21. 宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
宏命令是命令模式与组合模式的联用产物。
var closeDoorCommand = {
execute: function() {
console.log('关门')
},
}
var openPcCommand = {
execute: function() {
console.log('开电脑')
},
}
var openQQCommand = {
execute: function() {
console.log('登录 QQ')
},
}
var MacroCommand = function() {
return {
commandsList: [],
add: function(command) {
this.commandsList.push(command)
},
execute: function() {
for (var i = 0, command; (command = this.commandsList[i++]); ) {
command.execute()
}
},
}
}
var macroCommand = MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute()
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
# 22. 组合模式
组合模式将对象组合成树形结构,以表示 “部分——整体” 的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性,下面分别说明。
- 表示树形结构。通过回顾上面的例子,我们很容易找到组合模式的一个优点:提供了一种遍历树形结构的方案,通过调用组合对象的 execute 方法,程序会递归调用组合对象下面的叶对象的 execute 方法,所以我们的万能遥控器只需要一次操作,便能依次完成关门、打开电脑、登录 QQ 这几件事情。组合模式可以非常方便地描述对象部分——整体层次结构。
- 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。
var Folder = function(name) {
this.name = name
this.files = []
}
Folder.prototype.add = function(file) {
this.files.push(file)
}
Folder.prototype.scan = function() {
console.log('开始扫描文件夹: ' + this.name)
for (var i = 0, file, files = this.files; (file = files[i++]); ) {
file.scan()
}
}
var File = function(name) {
this.name = name
}
File.prototype.add = function() {
throw new Error('文件下面不能再添加文件')
}
File.prototype.scan = function() {
console.log('开始扫描文件: ' + this.name)
}
var folder = new Folder('学习资料')
var folder1 = new Folder('JavaScript')
var folder2 = new Folder('jQuery')
var file1 = new File('JavaScript 设计模式与开发实践')
var file2 = new File('精通 jQuery')
var file3 = new File('重构与模式')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
var folder3 = new Folder('Nodejs')
var file4 = new File('深入浅出 Node.js')
folder3.add(file4)
var file5 = new File('JavaScript 语言精髓与编程实践')
folder.add(folder3)
folder.add(file5)
folder.scan()
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
# 23. 何时使用组合模式
组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况。
- 表示对象的 部分——整体 层次结构。组合模式可以方便地构造一棵树来表示对象的 部分——整体 结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合 开放—封闭 原则。
- 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if 、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。
# 24. 模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
小结:在 JavaScript 中,我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择。
# 25. Coffee or Tea
- Beverage.prototype.init 被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。在 Beverage.prototype.init 方法中,算法内的每一个步骤都清楚地展示在我们眼前。
var Beverage = function() {}
Beverage.prototype.boilWater = function() {
console.log('把水煮沸')
}
Beverage.prototype.brew = function() {} // 空方法,应该由子类重写
Beverage.prototype.pourInCup = function() {} // 空方法,应该由子类重写
Beverage.prototype.addCondiments = function() {} // 空方法,应该由子类重写
// 模板方法
Beverage.prototype.init = function() {
this.boilWater()
this.brew()
this.pourInCup()
this.addCondiments()
}
var Coffee = function() {}
Coffee.prototype = new Beverage()
Coffee.prototype.brew = function() {
console.log('用沸水冲泡咖啡')
}
Coffee.prototype.pourInCup = function() {
console.log('把咖啡倒进杯子')
}
Coffee.prototype.addCondiments = function() {
console.log('加糖和牛奶')
}
var coffee = new Coffee()
coffee.init()
var Tea = function() {}
Tea.prototype = new Beverage()
Tea.prototype.brew = function() {
console.log('用沸水浸泡茶叶')
}
Tea.prototype.pourInCup = function() {
console.log('把茶倒进杯子')
}
Tea.prototype.addCondiments = function() {
console.log('加柠檬')
}
var tea = new Tea()
tea.init()
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
# 26. 真的需要 “继承” 吗
- 模板方法模式是为数不多的基于继承的设计模式,但 JavaScript 语言实际上没有提供真正的类式继承(ES6 有了 class),继承是通过对象与对象之间的委托来实现的。也就是说,虽然我们在形式上借鉴了提供类式继承的语言,但本章学习到的模板方法模式并不十分正宗。而且在 JavaScript 这般灵活的语言中,实现这样一个例子,是否真的需要继承这种重武器呢?
var Beverage = function(param) {
var boilWater = function() {
console.log('把水煮沸')
}
var brew =
param.brew ||
function() {
throw new Error('必须传递 brew 方法')
}
var pourInCup =
param.pourInCup ||
function() {
throw new Error('必须传递 pourInCup 方法')
}
var addCondiments =
param.addCondiments ||
function() {
throw new Error('必须传递 addCondiments 方法')
}
var F = function() {}
F.prototype.init = function() {
boilWater()
brew()
pourInCup()
addCondiments()
}
return F
}
var Coffee = Beverage({
brew: function() {
console.log('用沸水冲泡咖啡')
},
pourInCup: function() {
console.log('把咖啡倒进杯子')
},
addCondiments: function() {
console.log('加糖和牛奶')
},
})
var Tea = Beverage({
brew: function() {
console.log('用沸水浸泡茶叶')
},
pourInCup: function() {
console.log('把茶倒进杯子')
},
addCondiments: function() {
console.log('加柠檬')
},
})
var coffee = new Coffee()
coffee.init()
var tea = new Tea()
tea.init()
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
# 27. 享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly” 在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
小结:享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。
// 不使用享元模式
var Model = function(sex, underwear) {
this.sex = sex
this.underwear = underwear
}
Model.prototype.takePhoto = function() {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear)
}
for (var i = 1; i <= 50; i++) {
var maleModel = new Model('male', 'underwear' + i)
maleModel.takePhoto()
}
for (var j = 1; j <= 50; j++) {
var femaleModel = new Model('female', 'underwear' + j)
femaleModel.takePhoto()
}
// 使用享元模式(内部状态Model)
var Model = function(sex) {
this.sex = sex
}
Model.prototype.takePhoto = function() {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear)
}
var maleModel = new Model('male'),
femaleModel = new Model('female')
for (var i = 1; i <= 50; i++) {
maleModel.underwear = 'underwear' + i
maleModel.takePhoto()
}
for (var j = 1; j <= 50; j++) {
femaleModel.underwear = 'underwear' + j
femaleModel.takePhoto()
}
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
# 28. 内部状态与外部状态
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。
# 29. 享元模式的适用性
享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时 便可以使用享元模式。
- 一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 对象的大多数状态都可以变为外部状态。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
# 30. 通用对象池实现
- 对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。
var objectPoolFactory = function(createObjFn) {
var objectPool = []
return {
create: function() {
var obj =
objectPool.length === 0
? createObjFn.apply(this, arguments)
: objectPool.shift()
return obj
},
recover: function(obj) {
objectPool.push(obj)
},
}
}
var iframeFactory = objectPoolFactory(function() {
var iframe = document.createElement('iframe')
document.body.appendChild(iframe)
iframe.onload = function() {
iframe.onload = null // 防止 iframe 重复加载的 bug
iframeFactory.recover(iframe) // iframe 加载完成之后回收节点
}
return iframe
})
var iframe1 = iframeFactory.create()
iframe1.src = 'http:// baidu.com'
var iframe2 = iframeFactory.create()
iframe2.src = 'http:// QQ.com'
setTimeout(function() {
var iframe3 = iframeFactory.create()
iframe3.src = 'http:// 163.com'
}, 3000)
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
# 31. 职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。
小结:在 JavaScript 开发中,职责链模式是最容易被忽视的模式之一。实际上只要运用得当,职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。
# 32. 灵活可拆分的职责链节点
// 我们约定,如果某个节点不能处理请求,则返回一个特定的字符串 'nextSuccessor' 来表示该请求需要继续往后面传递
var order500 = function(orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500 元定金预购,得到 100 优惠券')
} else {
return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
}
}
var order200 = function(orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200 元定金预购,得到 50 优惠券')
} else {
return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
}
}
var orderNormal = function(orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function(fn) {
this.fn = fn
this.successor = null
}
Chain.prototype.setNextSuccessor = function(successor) {
return (this.successor = successor)
}
Chain.prototype.passRequest = function() {
var ret = this.fn.apply(this, arguments)
if (ret === 'nextSuccessor') {
return (
this.successor &&
this.successor.passRequest.apply(this.successor, arguments)
)
}
return ret
}
// 把 3个订单函数分别包装成职责链的节点
var chainOrder500 = new Chain(order500)
var chainOrder200 = new Chain(order200)
var chainOrderNormal = new Chain(orderNormal)
// 指定节点在职责链中的顺序
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
// 把请求传递给第一个节点
chainOrder500.passRequest(1, true, 500) // 输出:500 元定金预购,得到 100 优惠券
chainOrder500.passRequest(2, true, 500) // 输出:200 元定金预购,得到 50 优惠券
chainOrder500.passRequest(3, true, 500) // 输出:普通购买,无优惠券
chainOrder500.passRequest(1, false, 0) // 输出:手机库存不足
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
# 33. 中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
小结。
- 中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟 “城门失火,殃及池鱼” 的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。
- 因此,中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。
- 不过,中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。
function Player(name) {
this.name = name
this.enemy = null // 敌人
}
Player.prototype.win = function() {
console.log(this.name + ' won ')
}
Player.prototype.lose = function() {
console.log(this.name + ' lost')
}
Player.prototype.die = function() {
this.lose()
this.enemy.win()
}
// 接下来创建 2个玩家对象
var player1 = new Player('皮蛋')
var player2 = new Player('小乖')
// 给玩家相互设置敌人
player1.enemy = player2
player2.enemy = player1
player1.die() // 输出:皮蛋 lost、小乖 won
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 34. 装饰者模式
在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式,装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种 “即用即付” 的方式。
# 35. 模拟传统面向对象语言的装饰者模式
- 作为一门解释执行的语言,给 JavaScript 中的对象动态添加或者改变职责是一件再简单不过的事情,虽然这种做法改动了对象自身,跟传统定义中的装饰者模式并不一样,但这无疑更符合 JavaScript 的语言特色。传统面向对象语言中的装饰者模式在 JavaScript 中适用的场景并不多。
var obj = {
name: 'sven',
address: '深圳市',
}
obj.address = obj.address + '福田区'
2
3
4
5
- 传统面向对象语言中的装饰者模式实现。
// 原始的飞机类
var Plane = function() {}
Plane.prototype.fire = function() {
console.log('发射普通子弹')
}
// 装饰类
var MissileDecorator = function(plane) {
this.plane = plane
}
MissileDecorator.prototype.fire = function() {
this.plane.fire()
console.log('发射导弹')
}
var AtomDecorator = function(plane) {
this.plane = plane
}
AtomDecorator.prototype.fire = function() {
this.plane.fire()
console.log('发射原子弹')
}
var plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire()
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
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
# 36. 回到 JavaScript 的装饰者
- JavaScript 语言动态改变对象相当容易,我们可以直接改写对象或者对象的某个方法,并不需要使用 “类” 来实现装饰者模式。
var plane = {
fire: function() {
console.log('发射普通子弹')
},
}
var missileDecorator = function() {
console.log('发射导弹')
}
var atomDecorator = function() {
console.log('发射原子弹')
}
var fire1 = plane.fire
plane.fire = function() {
fire1()
missileDecorator()
}
var fire2 = plane.fire
plane.fire = function() {
fire2()
atomDecorator()
}
plane.fire()
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 37. 用 AOP 装饰函数
Function.prototype.before = function(beforefn) {
var __self = this // 保存原函数的引用
return function() {
// 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments) // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments) // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
}
}
Function.prototype.after = function(afterfn) {
var __self = this
return function() {
var ret = __self.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 数据统计上报。
<button tag="login" id="button">点击打开登录浮层</button>
Function.prototype.after = function(afterfn) {
var __self = this
return function() {
var ret = __self.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
var showLogin = function() {
console.log('打开登录浮层')
}
var log = function() {
console.log('上报标签为: ' + this.getAttribute('tag'))
}
showLogin = showLogin.after(log) // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 用 AOP 动态改变函数的参数。
Function.prototype.before = function(beforefn) {
var __self = this
return function() {
beforefn.apply(this, arguments) // (1)
return __self.apply(this, arguments) // (2)
}
}
// 示例一
var func = function(param) {
console.log(param) // 输出: {a: "a", b: "b"}
}
func = func.before(function(param) {
param.b = 'b'
})
func({ a: 'a' })
// 示例二
var getToken = function() {
return 'Token'
}
ajax = ajax.before(function(type, url, param) {
param.Token = getToken()
})
ajax('get', 'http:// xxx.com/userinfo', { name: 'sven' })
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 插件式的表单验证。
Function.prototype.before = function(beforefn) {
var __self = this
return function() {
if (beforefn.apply(this, arguments) === false) {
// beforefn 返回 false 的情况直接 return,不再执行后面的原函数
return
}
return __self.apply(this, arguments)
}
}
var validata = function() {
if (username.value === '') {
alert('用户名不能为空')
return false
}
if (password.value === '') {
alert('密码不能为空')
return false
}
}
var formSubmit = function() {
var param = {
username: username.value,
password: password.value,
}
ajax('http:// xxx.com/login', param)
}
formSubmit = formSubmit.before(validata)
submitBtn.onclick = function() {
formSubmit()
}
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
- 值得注意的是,因为函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。
var func = function() {
alert(1)
}
func.a = 'a'
func = func.after(function() {
alert(2)
})
alert(func.a) // 输出:undefined
2
3
4
5
6
7
8
# 38. 装饰者模式和代理模式
装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层 代理—本体 的引用,而装饰者模式经常会形成一条长长的装饰链。
在虚拟代理实现图片预加载的例子中,本体负责设置 img 节点的 src,代理则提供了预加载的功能,这看起来也是 “加入行为” 的一种方式,但这种加入行为的方式和装饰者模式的偏重点是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为,而代理做的事情还是跟本体一样,最终都是设置 src。但代理可以加入一些 “聪明” 的功能,比如在图片真正加载好之前,先使用一张占位的 loading 图片反馈给客户。
# 39. 状态模式
状态模式是一种非同寻常的优秀模式,它也许是解决某些需求场景的最好方法。虽然状态模式并不是一种简单到一目了然的模式(它往往还会带来代码量的增加),但你一旦明白了状态模式的精髓,以后一定会感谢它带给你的无与伦比的好处。
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
# 40. 状态模式的优缺点
状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支。
用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
# 41. 状态模式和策略模式的关系
状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法。而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为” 这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节,这正是状态模式的作用所在。
# 42. JavaScript 版本的状态机
var delegate = function(client, delegation) {
return {
buttonWasPressed: function() {
// 将客户的操作委托给 delegation 对象
return delegation.buttonWasPressed.apply(client, arguments)
},
}
}
var FSM = {
off: {
buttonWasPressed: function() {
console.log('关灯')
this.button.innerHTML = '下一次按我是开灯'
this.currState = this.onState
},
},
on: {
buttonWasPressed: function() {
console.log('开灯')
this.button.innerHTML = '下一次按我是关灯'
this.currState = this.offState
},
},
}
var Light = function() {
this.offState = delegate(this, FSM.off)
this.onState = delegate(this, FSM.on)
this.currState = this.offState // 设置初始状态为关闭状态
this.button = null
}
Light.prototype.init = function() {
var button = document.createElement('button'),
self = this
button.innerHTML = '已关灯'
this.button = document.body.appendChild(button)
this.button.onclick = function() {
self.currState.buttonWasPressed()
}
}
var light = new Light()
light.init()
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
# 43. 适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
小结:适配器模式是一对相对简单的模式。在本书提到的设计模式中,有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式。这几种模式都属于 “包装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。
- 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。
- 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
- 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。
# 44. 适配器模式的应用
- 如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种 “亡羊补牢” 的模式,没有人会在程序的设计之初就使用它。因为没有人可以完全预料到未来的事情,也许现在好好工作的接口,未来的某天却不再适用于新系统,那么我们可以用适配器模式把旧接口包装成一个新的接口,使它继续保持生命力。
# 45. 单一职责原则
单一职责原则(SRP)的职责被定义为 “引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
SRP 原则体现为:一个对象(方法)只做一件事情。
SRP 原则在很多设计模式中都有着广泛的运用,例如代理模式、迭代器模式、单例模式和装饰者模式。
# 46. SRP 原则的优缺点
SRP 原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。
但 SRP 原则也有一些缺点,最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。
# 47. 最少知识原则
最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。
其实,最少知识原则也叫迪米特法则(Law of Demeter,LoD),“迪米特” 这个名字源自 1987 年美国东北大学一个名为 “Demeter”的研究项目。
最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式。
# 48. 开放 — 封闭原则
在面向对象的程序设计中,开放—封闭原则(OCP)是最重要的一条原则。很多时候,一个程序具有良好的设计,往往说明它是符合开放—封闭原则的。
开放—封闭原则最早由 Eiffel 语言的设计者 Bertrand Meyer 在其著作 Object-Oriented SoftwareConstruction 中提出。它的定义如下。
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
开放—封闭原则的思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码(hook 与 callback)。
# 49. 设计模式中的开放 — 封闭原则
- 有一种说法是,设计模式就是给做的好的设计取个名字。几乎所有的设计模式都是遵守开放—封闭原则的,我们见到的好设计,通常都经得起开放—封闭原则的考验。不管是具体的各种设计模式,还是更抽象的面向对象设计原则,比如单一职责原则、最少知识原则、依赖倒置原则等,都是为了让程序遵守开放—封闭原则而出现的。可以这样说,开放—封闭原则是编写一个好程序的目标,其他设计原则都是达到这个目标的过程。