原型模式
原型模式不关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一摸一样的对象。
var Plane = function() {
this.blood = 100;
this.attackLevel = -1;
this.defenseLevel = -1;
}
var plane = new Plane()
plane.blood = 500;
plane.attackLevel = 4;
plane.defenseLevel = 5;
// 通过克隆创建一个一摸一样的对象
var clonePlane = Object.create(plane)
console.log(clonePlane.blood) // 500
console.log(clonePlane.attackLevel) // 4
console.log(clonePlane.defenseLevel) // 5
原型模式的规则:
- 所有的数据都是对象。
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型。
- 如果对象无法响应某个请求,他会吧这个请求委托给它自己的原型。
单例模式
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
下面是一个普通单例的例子:
var Person = function (name) {
this.name = name
this.instance = null
}
Person.prototype.getName = function () {
return this.name
}
Person.getInstance = function (name) {
if (!this.instance) {
this.instance = new Person(name)
}
return this.instance
}
var a = Person.getInstance('sven')
var b = Person.getInstance('sven2')
console.log(a === b) // true
上面单例每次都要靠Person.getInstance()来实现,使用的时候不怎么友好,因为我们创建实例的时候习惯直接使用new运算符,所以接下来出现一个改进版:
var Person= (function () {
var instance;
var Person= function (name) {
if (instance) {return instance}
this.name = name
return instance = this
}
Person.prototype.getName = function(){
return this.name
}
return Person
})()
var a = new Person('yy')
var b = new Person('zz')
console.log(a === b) // true
上面这个例子还有一点问题,就是这个函数只能是单例了,如果我以后需要这个函数实现多例,那就要将Person中控制创建唯一对象的相关代码删除,这种修改会存在很多的隐患。
为了避免上述的隐患,我们继续来改进这个例子:
var Person = function(name) {
this.name = name
}
Person.prototype.getName = function () {return this.name}
// 使用一个代理来完成唯一对象的创建
var proxySingletonPerson = (function () {
var instance;
return function (name) {
if (!instance) {
instance = new Person(name)
}
return instance
}
})()
var a = new proxySingletonPerson('yy')
var b = new proxySingletonPerson('zz')
console.log(a === b) // true
观察代理函数会发现主要用于变成单例的部分是这个模式
var obj
if (obj) {
obj = xxxx
}
所以我们可以再抽象出一层:
var Person = function(name) {
this.name = name
}
Person.prototype.getName = function() {
return this.name
}
var getSingle = function(fn) {
var result;
return function() {
return result || (result = new fn(arguments))
}
}
var singletonPerson = getSingle(Person)
var a = new singletonPerson('a')
var b = new singletonPerson('bbb')
console.log(a === b) // true
策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
下面是一个策略模式的例子:
完成一段路程,可以走路、自行车或者汽车,不同的方式有不同的速度,要得到不同方式对应的完成时间。
var strategies = {
'walk': function (distance) {
return distance / 5
},
'bike': function (distance) {
return distance / 15
},
'car': function (distance) {
return distance / 30
}
}
var calculateTime = function (way, distance) {
return strategies[way](distance)
}
console.log(calculateTime('walk', 1000)) // 200
console.log(calculateTime('bike', 1000)) // 66.67
console.log(calculateTime('car', 1000)) // 33.33
不同的出行方式对应不同的算法,并且当需要添加的新的出行方式的时候不用修改calculateTime,就不会对之前的内容造成影响了。
代理模式
代理模式是为对象提供一个代用品或占位符,以便控制对它的访问。
代理模式有多种形式,保护代理、虚拟代理、缓存代理、防火墙代理、远程代理等等。在JavaScript中,一般比较常用的是虚拟代理和缓存代理。
虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。
下面看一个虚拟代理的例子:
var myImage = (function() {
var imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return {
setSrc: function(src) {
imgNode.src = src
}
}
})()
var proxyImage = (function() {
var img = new Image
img.onload = function() {
myImage.setSrc(this.src)
}
return {
setSrc: function(src) {
myImage.setSrc('loading.gif')
img.src = src
}
}
})()
proxyImage.setSrc('pucture.png')
上面这个例子中,使用的是图片预加载技术,先在proxyImage中加载需要的图片,然后让myImage显示loading图片,等需要的图片完全加载完毕后,再替换掉loading图片。
那为什么要将两个功能分开,它们应该可以合并为一个函数才对。这是因为单一职责原则(就一个类而言,应该仅有一个引起它变化的原因),面向对象设计中,一个对象承担的职责越多,那引起它变化的原因就会变得越多,当变化发生时,设计可能会遭到意外的破坏。
两个函数的使用都是setSrc接口,所以这样当不需要预加载的时候,可以直接请求对象本体,理解与修改也非常方便。
虚拟代理还有合并http请求、让文件实现惰性加载等能力。(惰性加载实例见JavaScript设计模式与开发实践6.7节)
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
var mult = function() {
console.log('开始计算乘积')
var a = 1
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i]
}
return a
}
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)
}
})()
console.time('a')
proxyMult(1, 2, 3, 4, 5)
console.timeEnd('a') // 3ms以上
console.time('b')
proxyMult(1, 2, 3, 4, 5)
console.timeEnd('b') // 1ms以下
上例中,计算结果被缓存,所以相同参数传入后不需要再进行计算直接读取就可以了,所以比较节约时间。
迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
按照上面的定义,数组的forEach、reduce、reduceRight、filter这些应该都属于迭代函数,首先我们实现一个forEach函数
var each = function(arr, fn) {
var l = arr.length,
i = 0;
for (; i < l; i++) {
fn(arr[i], i, arr)
}
}
each([1,2,3,4,5], function (e) {
console.log(e)
})
each属于内部迭代器,因为each函数的内部已经定义好了迭代规则,就是对一个数组内容进行遍历并分别调用函数fn。如果需要对两个数组进行处理,那么就需要对迭代规则进行修改。
下面来看一个外部迭代器:
/* 迭代器主体 */
var Iterator = function (obj) {
var current = 0
var next = function () {
current += 1
}
var getCurrItem = function () {
return obj[current]
}
var isDone = function () {
return current >= obj.length
}
return {
next: next,
getCurrItem: getCurrItem,
isDone: isDone
}
}
var each = function (iterator) {
while(!iterator.isDone()) {
/* 这里是循环遍历的主题,也就是一般each函数中自定义的fn */
console.log(iterator.getCurrItem())
iterator.next()
}
}
var iterator1 = Iterator([1,2,3,4,5])
each(iterator1)
发布-订阅模式(观察者模式)
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript中,我们一般用事件模型来替代传统的发布-订阅模式。
发布-订阅模式需要哪些东西:
- 订阅者列表
- 订阅的方法
- 发布的方法
var event = {
// 消息列表
clientList: [],
// 订阅消息
subscribe: function(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = []
}
this.clientList[key].push(fn)
},
// 发布消息
publish: function() {
var key = Array.prototype.shift.call(arguments),
fns = this.clientList[key]
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; fn = fns[i++]) {
fn.apply(this, arguments)
}
}
}
// 让对象拥有这些方法
function installEvent(obj) {
for ( var i in event) {
obj[i] = event[i]
}
}
var demo = {}
installEvent(demo)
demo.subscribe('milk', function (num) {
console.log('牛奶新到:' + num)
})
demo.subscribe('milk2', function (num) {
console.log('牛奶2新到:' + num)
})
demo.publish('milk', 100)
demo.publish('milk2', 120)
上面是发布-订阅模式简单的示例,订阅者设置需要的消息和回调函数,发布者在发布消息的时候触发相应消息中的函数并将参数传入。
命令模式
命令模式是最简单和优雅的模式之一,命令模式种的命令指的是一个执行某些特定事情的指令。
命令模式最常见的场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁也不知道被请求的操作是上面。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够清除彼此之间的耦合关系。
// 将命令封装成相应的对象
var OpenTvCommand = function (receiver) {this.receiver = receiver;}
OpenTvCommand.prototype.execute = function () {this.receiver.open()}
OpenTvCommand.prototype.undo = function () {this.receiver.close()}
// 设置命令的请求者需要执行的命令
var setCommand = function (command) {
execute.onclick = function () {command.execute()}
undo.onclick = function () {command.undo()}
}
// 将命令的请求者和接收者结合在一起
setCommand( new OpenTvCommand(Tv))
命令模式有时候和策略模式很像,区别是策略模式中所有策略的目标都是相同的,只是内部的“算法”有区别,但命令模式中的命令并不是针对一个目标,命令模式能完成更多的功能。
组合模式
组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。
// 普通命令
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.commandList.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()
观察上例可以发现:我们通过一个函数,将多个命令组合成了对象,并且单条命令的执行方式和组合命令的执行方式相同;组合命令的结构类似于树形结构,而且执行的顺序可以看做是对树深度优先的搜索。
组合模式的使用场景一般有两种:
- 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。
- 客户希望统一对待树中的所有对象。
模板方法模式
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
抽象类:在Java中,类分为具体类和抽象类两种。具体类可以被实例化,抽象类不能被实例化。因为抽象类不能被实例化,所以抽象类一定是用来被某些具体类继承的。
标准的模板方法实现见JavaScript设计模式与开发实践11.2节。
适合JS的实现:
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()
在上例中,Beverage 是抽象类,Coffee和Tea是具体类,Beverage中的F.init()封装了子类的算法框架,指导子类以何种顺序执行哪些方法,所以F.init()是模板方法。Beverage 将Coffee和Tea中都要执行的烧水过程确定,并定义好了Coffee和Tea中具体方法的执行顺序,也就是将两个类中的通用部分进行封装,提高了函数的扩展性。
享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态,而外部状态取决于具体的场景,并根据场景而变化。
内部状态和外部状态:
- 内部状态存储与对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
var Upload = function(uploadType) {
this.uploadType = uploadType
}
Upload.prototype.delFile = function(id) {
uploadManager.setExternalState(id, this)
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('确定要删除该文件吗?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom)
}
}
// 工厂模式进行对象实例化,根据内部对象的数量来创建出新对象
var UploadFactory = (function() {
// 保存对象
var createdFlyWeightObjs = {}
return {
create: function(uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType]
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType)
}
}
})()
// 管理器封装外部状态
var uploadManager = (function() {
// 用于保存外部状态
var uploadDatabase = {}
return {
add: function(id, uploadType, fileName, fileSize) {
var flyWeightObj = UploadFactory.create(uploadType)
var dom = document.createElement('div')
dom.innerHTML = '文件名称:' + fileName + '文件大小:' + fileSize + '' + ''
dom.querySelector('.delFile').onclick = function() {
flyWeightObj.delFile(id)
}
document.body.appendChild(dom)
uploadDatabase[id] = {
fileName: fileName,
fileSize: fileSize,
dom: dom
}
return flyWeightObj
},
// 通过id来返回对应的外部状态
setExternalState: function(id, flyWeightObj) {
var uploadData = uploadDatabase[id]
for (var i in uploadData) {
flyWeightObj[i] = uploadData[i]
}
}
}
})()
var id = 0
window.startUpload = function(uploadType, files) {
for (var i = 0, file; file = files[i++];) {
var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
}
}
startUpload('plugin', [{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.txt',
fileSize: 3000
},
{
fileName: '3.txt',
fileSize: 5000
},
])
startUpload('flash', [{
fileName: '4.txt',
fileSize: 1000
},
{
fileName: '5.txt',
fileSize: 3000
},
{
fileName: '6.txt',
fileSize: 5000
},
])
职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象成为链中的节点。
举例:我口袋有一些钱,想买饮料,当在10块以下的时候,买矿泉水;当20块以下的时候,买奶茶;当30块以下的时候,买咖啡。
var drink = function(money) {
if (money < 10) {
console.log('矿泉水')
} else if (money < 20) {
console.log('奶茶')
} else {
console.log('咖啡')
}
}
通过上面这个函数,我们能得到正确的结果,但这段代码并不值得称赞,当条件变化的时候,我们需要不断修改这个函数,会导致越来越难维护。所以接下来我们使用职责链模式来处理这件事情:
/* 将不同条件分开 */
var drinkWater = function(money) {
if (money > 0 && money <= 10) {
console.log('矿泉水')
} else {
return 'nextSuccessor'
}
}
var drinkMilkTea = function(money) {
if (money > 10 && money <= 20) {
console.log('奶茶')
} else {
return 'nextSuccessor'
}
}
var drinkCoffee = function(money) {
if (money > 20 && money <= 50) {
console.log('咖啡')
} else {
return 'nextSuccessor'
}
}
/* 定义职责链传递的方式 */
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
}
/* 开始使用 */
var chainWater = new Chain(drinkWater)
var chainMilkTea = new Chain(drinkMilkTea)
var chainCoffee = new Chain(drinkCoffee)
chainWater.setNextSuccessor(chainMilkTea)
chainMilkTea.setNextSuccessor(chainCoffee)
chainWater.passRequest(4) // 矿泉水
chainWater.passRequest(14) // 奶茶
chainWater.passRequest(24) // 咖啡
可以看出函数干净了很多,现在要修改饮料种类的话就不用管原来函数的内容是什么,直接增加一个函数然后添加到职责链里面就好。
接下来再看一个比较方便的实现方式:
Function.prototype.after = function(fn) {
var self = this
return function() {
var ret = self.apply(this, arguments)
if (ret === 'nextSuccessor') {
return fn.apply(this, arguments)
}
return ret
}
}
var drinkWater = function(money) {
if (money > 0 && money <= 10) {
console.log('矿泉水')
} else {
return 'nextSuccessor'
}
}
var drinkMilkTea = function(money) {
if (money > 10 && money <= 20) {
console.log('奶茶')
} else {
return 'nextSuccessor'
}
}
var drinkCoffee = function(money) {
if (money > 20 && money <= 50) {
console.log('咖啡')
} else {
return 'nextSuccessor'
}
}
var drink = drinkWater.after(drinkMilkTea).after(drinkCoffee)
drink(20) // 奶茶
after的实现使用了递归的思想,类似于二叉树搜索的深度优先,将需要执行的函数按顺序排列好,再开始逐个执行调用。
中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
中介者模式和发布-订阅模式很像,区别就是发布-订阅模式中发布者只能发布消息,订阅者只能接收消息,中介者模式中,每个模块都能够对中介者发布消息,也能从中介者中接收到消息。
下面看一个例子:
微信群聊中,所有人都能发送消息,也能接收消息,一个人发送消息的时候,其他人就会收到消息通知。
// 创建用户相关方法
function User(name) {
this.name = name
}
User.prototype.receiveMessage = function(people) {
console.log(this.name + '收到消息:' + people.name + "说: " + people.msg)
}
User.prototype.sendMessage = function(msg) {
this.msg = msg ? msg : ''
wechart.ReceiveMessage('sendMessage', this)
}
User.prototype.leaveRoom = function() {
wechart.ReceiveMessage('leaveRoom', this)
}
// 创建中介者
var wechart = (function() {
var users = [],
operations = {}
operations.addUser = function(user) {
var isRepeat = users.some(function(u) {
return u.name === user.name
})
if (!isRepeat) {
users.push(user)
console.log(user.name + " 加入群聊")
}
}
operations.sendMessage = function(sender) {
users.forEach(function(user) {
if (user.name !== sender.name) {
user.receiveMessage(sender)
}
})
}
operations.leaveRoom = function(user) {
var i = 0,
l = users.length
for (; i < l; i++) {
if (users[i].name === user.name) {
users.splice(i, 1)
console.log('系统:' + user.name + " 退出群聊")
break
}
}
}
var ReceiveMessage = function() {
var eventName = Array.prototype.shift.call(arguments)
operations[eventName].apply(this, arguments)
}
return {
ReceiveMessage: ReceiveMessage
}
})()
// 通过工厂函数来创建用户
var chartRoom = function(name) {
var newUser = new User(name)
wechart.ReceiveMessage('addUser', newUser)
return newUser
}
var user1 = chartRoom('小明')
var user2 = chartRoom('小红')
var user3 = chartRoom('小强')
var user4 = chartRoom('小刚')
user1.sendMessage('hello') // 小红收到消息:小明说:hello
// 小强收到消息:小明说:hello
// 小刚收到消息:小明说:hello
user1.leaveRoom() // 系统:小明 退出群聊
在上例中,wechart就是中介者对象,它负责接收消息并分发给所有用户,它的存在,让用户和用户之间没有了耦合关系,用户要做什么,只要通知中介者,中介者就能处理完消息后把结果返回给其他的玩家对象。从这里可以明显看出,中介者模式和订阅发布模式有些类似,但还是不一样的。
中介者模式的优点是非常方便地对模块或者对象进行解耦,但有个缺点就是随着功能的逐渐复杂,中介者会变得越来越庞大,也就会变得越来越难以维护。