JavaScript设计模式与开发实践
Posted A_山水子农
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript设计模式与开发实践相关的知识,希望对你有一定的参考价值。
最近在研读了腾讯AlloyTeam前端团队,高级工程师曾探编写的《javascript设计模式与开发实践》,所有设计模式的实现都遵循一条原则,即“找出程序中变化的地方,并将变化封装起来”。一个程序的设计总是可以分为可变的部分和不变的部分。当我们找出可变的部分,并且把这部分封装起来,那么剩下的就是不变和稳定的部分。
JavaScript没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。JavaScript也没有在语言层面提供对抽象类和接口的支持。所以在JavaScript用设计模式编写代码的时候,要跟传统面向对象语言加以区分。
1、单例模式
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全军缓存、浏览器中的window对象等。
<html>
<head>
<title>惰性单例-点击登录弹出登录浮窗</title>
</head>
<body>
<button id="loginBtn">登录</button>
<script>
// 创建实例对象的职责
let createLoginLayer = function ()
let div = document.createElement('div')
div.innerHTML = '我是登录浮窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
let createIframeLayer = function ()
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe
// 管理单例的职责
let getSingle = function (fn)
let result
return function ()
return result || (result = fn.apply(this, arguments))
// 创建div浮窗
let createSingleLoginLayer = getSingle(createLoginLayer)
// 点击多次都只会创建一个新的登录浮层div
document.getElementById('loginBtn').onclick = function ()
let loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block';
</script>
</body>
</html>
2、策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式的目的就是将算法的使用和算法的实现分离开来。说的更详细点就是:定义一些列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。
// 普通实现
let calculateBonusCommon = (performanceLevel, salary) =>
if (performanceLevel === 'S')
return salary * 4
if (performanceLevel === 'A')
return salary * 3
if (performanceLevel === 'B')
return salary * 2
console.log(calculateBonusCommon('B', 20000))
console.log(calculateBonusCommon('S', 50000))
// 策略模式实现
// 策略类
let strategies =
'S': (salary) => salary * 4,
'A': (salary) => salary * 3,
'C': (salary) => salary * 2
// 环境类
let calculateBonus = (level, salary) => strategies[level](salary)
console.log(calculateBonus('S', 20000))
console.log(calculateBonus('A', 10000))
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。
3、代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
<html>
<head>
<title>虚拟代理实现图片预加载</title>
</head>
<body>
<script>
// 加载图片
let myImage = (function ()
let imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return
setSrc: function (src)
imgNode.src = src
)()
// 代理对象proxyImage
let proxyImage = (function ()
let img = new Image
img.onload = function ()
myImage.setSrc(this.src)
return
setSrc: function (src)
myImage.setSrc('./loading.gif')
img.src = src
)()
proxyImage.setSrc('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1521956229659&di=36a2ea375f48e8328b3cab79e8b1ea0e&imgtype=0&src=http%3A%2F%2Ff0.topitme.com%2F0%2Fa9%2F3e%2F1164210455aae3ea90o.jpg')
</script>
</body>
</html>
如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的。
// 乘积
let mult = function ()
console.log('开始计算乘积')
let a = 1
for (let i = 0, l = arguments.length; i < l; i++)
a *= arguments[i]
return a
// 加和
let plus = function ()
console.log('开始计算加和')
let a = 1
for (let i = 0, l = arguments.length; i < l; i++)
a += arguments[i]
return a
// 减法
let subtraction = function ()
console.log('开始计算减法')
let a = 1
for (let i = 0, l = arguments.length; i < l; i++)
a -= arguments[i]
return a
// 缓存代理函数
let proxyMult = (function ()
let cache = []
return function ()
let args = Array.prototype.join.call(arguments, ',')
if (args in cache)
return cache[args]
return cache [args] = mult.apply(this, arguments)
)()
console.log(proxyMult(1, 2, 3, 4))
console.log(proxyMult(1, 2, 3, 4))
console.log(proxyMult(1, 2, 3, 4, 5))
// 高阶函数动态创建缓存代理的工厂
let createProxyFactory = function (fn)
let cache = []
return function ()
let args = Array.prototype.join.call(arguments, ',')
for (args in cache)
return cache[args]
return cache[args] = fn.apply(this, arguments)
let proxyPlus = createProxyFactory(plus)
let proxySubtrsction = createProxyFactory(subtraction)
console.log(proxyPlus(1, 2, 3, 4))
console.log(proxyPlus(1, 2, 3, 4))
console.log(proxySubtrsction(10, 3, 4))
console.log(proxySubtrsction(10, 3, 4))
4、迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
let each = function (ary, callback)
for (let i = 0, l = ary.length; i < l; i++)
callback.call(ary[i], i, ary[i])
each([1, 2, 3, 4], function(i, n)
console.log([i, n])
)
5、发布-订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来代替传统的发布-订阅模式。
下面看实现发布-订阅模式的步骤:
- 首先要指定好谁充当发布者。
- 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者。
- 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数
// 全局发布-订阅对象
let Event = (function ()
// 缓存列表,存放订阅者的回调函数
let clientList =
let listen, trigger, remove
// 增加订阅者
listen = (key, fn) =>
if (!clientList[key])
clientList[key] = []
clientList[key].push(fn)
,
// 发布消息
trigger = (...value) =>
let key = Array.prototype.shift.call(value)
let fns = clientList[key]
// 如果没有绑定的对应的消息
if (!fns || fns.length === 0)
return false
for ( let i = 0, fn; fn = fns[i++]; )
fn.apply(this, value)
,
// 取消订阅事件
remove = (key, fn) =>
let fns = clientList[key]
// 如果key对应的消息没有被人订阅,则直接返回
if (!fns)
return false
if (!fn) // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
fns && (fns.length = 0)
else
for (let l = fns.length - 1; l >= 0; l--)
let _fn = fns[l]
if (_fn === fn)
fns.splice(l, 1) // 删除订阅者的回调函数
return
listen,
trigger,
remove
)()
// 小明订阅消息
Event.listen('squareMeter88', fn1 = function (price, squareMeter)
console.log('小明先生:')
console.log('price = ' + price)
console.log('squareMeter = ' + squareMeter)
)
// 小红订阅消息
Event.listen('squareMeter88', fn2 = function (price, squareMeter)
console.log('小红小姐:')
console.log('price = ' + price)
console.log('squareMeter = ' + squareMeter)
)
// 售楼处发布消息
Event.trigger('squareMeter88', 10000, 88)
Event.remove('squareMeter88', fn1)
Event.trigger('squareMeter88', 15000, 88)
6、命令模式
命令模式中的命令(command)指的是一个执行某些特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
let button1 = document.getElementById('button1')
let button2 = document.getElementById('button2')
let button3 = document.getElementById('button3')
let MenuBar =
refresh: function()
console.log('刷新菜单界面')
let SubMenu =
add: function ()
console.log('增加子菜单')
,
del: function ()
console.log('删除子菜单')
// 接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可
let RefreshMenuBarCommand = function (receiver)
return
execute: function ()
receiver.refresh()
let AddSubMenuBarCommand = function (receiver)
return
execute: function ()
receiver.add()
let DelSubMenuBarCommand = function (receiver)
return
execute: function ()
receiver.del()
// setCommand函数负责往按钮上面安装命令
let setCommand = function (button, command)
button.onclick = function ()
command.execute()
let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
setCommand(button1, refreshMenuBarCommand)
let addSubMenuBarCommand = AddSubMenuBarCommand(SubMenu)
setCommand(button2, addSubMenuBarCommand)
let delSubMenuBarCommand = DelSubMenuBarCommand(SubMenu)
setCommand(button2, delSubMenuBarCommand)
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
let closeDoorCommand =
execute: function ()
console.log('关门')
let openPcCommand =
execute: function ()
console.log('开电脑')
let openQQCommand =
execute: function ()
console.log('登录QQ')
let MacroCommand = function ()
return
commandsList: [],
add: function (command)
this.commandsList.push(command)
,
execute: function()
for (let i =0, command; command = this.commandsList[i++];)
command.execute()
let macroCommand = MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute()
7、组合模式
组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
// 组合模式-扫描文件夹
// 文件夹
let 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(let i = 0, file; file = this.files[i++];)
file.scan()
// 文件
let File = function (name)
this.name = name
File.prototype.add = function ()
throw new Error('文件下面不能再添加文件')
File.prototype.scan = function ()
console.log('开始扫描文件:' + this.name)
// 创建文件夹
let folder = new Folder('学习资料')
let folder1 = new Folder('Javascript')
let folder2 = new Folder('JQuery')
// 创建文件
let file1 = new File('Javascript 设计模式与开发实践')
let file2 = new File('精通JQuery')
let file3 = new File('重构与模式')
folder1.add(file2)
folder2.add(file3)
folder.add(file1)
folder.add(file2)
folder.add(file3)
folder.add(folder1)
folder.add(folder2)
folder.scan()
8、模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。
let Beverage = function ()
Beverage.prototype.boilWater = function ()
console.log('把水煮沸')
Beverage.prototype.brew = function ()
throw new Error('子类必须重写brew方法')
Beverage.prototype.pourInCup = function ()
throw new Error('子类必须重写pourInCup方法')
Beverage.prototype.addCondiments = function ()
throw new Error('子类必须重写addCondiments方法')
// 钩子方法
Beverage.prototype.customerWantsCondiments = function ()
return true // 默认需要调料
// 模板方法
Beverage.prototype.init = function ()
this.boilWater()
this.brew()
this.pourInCup()
// 如果挂钩返回true。则需要调料
if (this.customerWantsCondiments())
this.addCondiments()
// 泡茶
let Tea = function ()
Tea.prototype = new Beverage()
Tea.prototype.brew = function ()
console.log('用沸水浸泡茶叶')
Tea.prototype.pourInCup = function ()
console.log('把茶倒进杯子')
Tea.prototype.addCondiments = function ()
console.log('加柠檬')
Tea.prototype.customerWantsCondiments = function ()
return window.confirm('请问需要调料吗?')
let tea = new Tea()
tea.init()
//泡咖啡
let Coffee = function ()
Coffee.prototype = new Beverage()
Coffee.prototype.brew = function ()
console.log('用沸水冲泡咖啡')
Coffee.prototype.pourInCup = function ()
console.log('把咖啡倒进杯子')
Coffee.prototype.addCondiments = function ()
console.log('加牛奶和糖')
let coffee = new Coffee()
coffee.init()
利用好莱坞原则,下面的代码可以达到和继承一样的效果
let Beverage = function (param)
let boilWater = function ()
console.log('把水煮沸')
let brew = param.brew || function ()
throw new Error('必须传递brew方法')
let pourInCup = param.pourInCup || function ()
throw new Error('必须传递pourInCup方法')
let addCondiments = param.addCondiments || function ()
throw new Error('必须传递addCondiments方法')
let customerWantsCondiments = param.customerWantsCondiments ? true : false
let F = function ()
// 模板方法
F.prototype.init = function ()
boilWater()
brew()
pourInCup()
if( customerWantsCondiments)
addCondiments()
return F
let Coffee = Beverage(
brew: function ()
console.log('用沸水冲泡咖啡')
,
pourInCup: function ()
console.log('把咖啡倒入杯子')
,
addCondiments: function ()
console.log('加糖和牛奶')
)
let coffee = new Coffee()
coffee.init()
let Tea = Beverage (
brew: function ()
console.log('用沸水泡茶')
,
pourInCup: function ()
console.log('把茶倒入杯子')
,
customerWantsCondiments: false
)
let tea = new Tea()
tea.init()
9、享元模式
享元模式是一种用于性能优化的模式。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
享元模式要求将对象的属性划分为内部状态和外部状态,把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并存储在外部。
<html>
<head>
<title>享元模式文件上传</title>
</head>
<body>
<script>
// uploadType是内部状态,fileName和fileSize是根据场景而变化,每个文件fileName和
// fileSize都不一样,fileName和fileSize没有办法被共享,它们只能被划分为外部状态
let 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)
// 工厂进行对象实例化,如果某种内部状态对应的共享对象已经被创建过,那么直接返回
// 这个对象,否则创建一个新的对象
let UploadFactory = (function ()
let createFlyWeightObjs =
return
create: function (uploadType)
if (createFlyWeightObjs[uploadType])
return createFlyWeightObjs[uploadType]
return createFlyWeightObjs[uploadType] = new Upload(uploadType)
)()
// 管理器封装外部状态
let uploadManager = (function ()
// 保存所有upload对象的外部状态
let uploadDatabase =
return
add: function (id, uploadType, fileName, fileSize)
let flyWeightObj = UploadFactory.create(uploadType)
let dom = document.createElement('div')
dom.innerHTML = '<span>文件名称:' + fileName + ',文件大小:' + fileSize + '</span>' +
'<button class="delFile">删除</button>'
dom.querySelector('.delFile').onclick = function ()
flyWeightObj.delFile(id)
document.body.appendChild(dom)
uploadDatabase[id] =
fileName: fileName,
fileSize: fileSize,
dom: dom
return flyWeightObj
,
setExternalState: function (id, flyWeightObj)
let uploadData = uploadDatabase[id]
for (let i in uploadData)
flyWeightObj[i] = uploadData[i]
)()
// 触发上传动作
let id = 0
window.startUpload = function (uploadType, files)
for (let i = 0, file; file = files[i++];)
let uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
startUpload('plugin', [
fileName: '1.txt',
fileSize: 1000
,
fileName: '2.html',
fileSize: 3000
,
fileName: '3.txt',
fileSize: 5000
])
startUpload('flash', [
fileName: '4.txt',
fileSize: 1000
,
fileName: '5.html',
fileSize: 3000
,
fileName: '6.txt',
fileSize: 5000
])
</script>
</body>
</html>
10、职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
let order500 = function (orderType, pay, stock)
if (orderType === 1 && pay)
console.log('500元定金预购,得到100优惠券')
else
return 'nextSuccessor'
let order200 = function (orderType, pay, stock)
if(orderType === 2 && pay)
console.log('200元定金预购,得到50优惠券')
else
return 'nextSuccessor'
let orderNomal = function (orderType, pay, stock)
if (stock > 0)
console.log('普通购买,无优惠券')
else
console.log('手机库存不足')
// 用AOP实现职责链
Function.prototype.after = function (fn)
let self = this
return function ()
let ret = self.apply(this, arguments)
if (ret === 'nextSuccessor')
return fn.apply(this, arguments)
return ret
let order = order500.after(order200).after(orderNomal)
order(1, true, 500)
order(2, true, 500)
order(3, true, 500)
order(1, false, 0)
11、中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各个对象之间耦合松散,而且可以独立地改变它们之间的交互。
/**
* @description player对象的原型方法中,不负责具体的执行逻辑,而是把操作转交给中介者对象。
* @param any name
* @param any teamColor
*/
function Player (name, teamColor)
this.state = 'live' // 玩家状态
this.name = name // 角色名字
this.teamColor = teamColor // 队伍颜色
// 玩家胜利
Player.prototype.win = function ()
console.log('winner: ' + this.name)
// 玩家失败
Player.prototype.lose = function ()
console.log('loser: ' + this.name)
// 玩家死亡
Player.prototype.die = function ()
this.state = 'dead'
// 给中介者发送消息,玩家死亡
playerDirector.ReceiveMessage('playerDead', this)
// 移除玩家
Player.prototype.remove = function ()
// 给中介者发送消息,移除一个玩家
playerDirector.ReceiveMessage('removePlayer', this)
// 玩家换队
Player.prototype.changeTeam = function (color)
// 给中介者发送消息,玩家换队
playerDirector.ReceiveMessage('changeTeam', this, color)
/**
* @description 工厂函数
*/
let playerFactory = function (name, teamColor)
// 创造一个新的玩家对象
let newPlayer = new Player(name, teamColor)
// 给中介者发送消息,新增玩家
playerDirector.ReceiveMessage('addPlayer', newPlayer)
return newPlayer
/**
* @description 中介者
*/
let playerDirector = (function ()
// 保存所有玩家
let players =
// 中介者可以执行的操作
let operations =
/******************* 新增一个玩家 ******************/
operations.addPlayer = function (player)
// 玩家队伍的颜色
let teamColor = player.teamColor
// 如果该颜色的玩家还没有成立队伍,则新成立一个队伍
players[teamColor] = players[teamColor] || []
// 添加玩家进队伍
players[teamColor].push(player)
/******************* 移除一个玩家 ******************/
operations.removePlayer = function (player)
// 玩家的队伍颜色
let teamColor = player.teamColor
// 该队伍的所有成员
let teamPlayers = players[teamColor] || []
// 遍历删除
for (let i = teamPlayers.length - 1; i >=0; i--)
if(teamPlayers[i] === player)
teamPlayers.splice(i, 1)
/******************* 玩家换队 ******************/
operations.changeTeam = function (player, newTeamColor)
operations.removePlayer(player)
player.teamColor = newTeamColor
operations.addPlayer(player)
/******************* 玩家死亡 ******************/
operations.playerDead = function (player)
// 玩家的队伍颜色
let teamColor = player.teamColor
// 玩家所在队伍
let teamPlayers = players[teamColor]
let all_dead = true
for (let i = 0, player; player = teamPlayers[i++];)
if (player.state !== 'dead')
all_dead = false
break
if (all_dead)
// 本队所有玩家都输了
for(let i = 0, player; player = teamPlayers[i++];)
player.lose()
for (let color in players)
if (color !== teamColor)
// 其他队伍的玩家
let teamPlayers = players[color]
// 其他队伍所有玩家胜利
for (let i = 0, player; player = teamPlayers[i++];)
player.win()
/********* 负责接收player对象发送的消息 *********/
let ReceiveMessage = function ()
let message = Array.prototype.shift.call(arguments)
operations[message].apply(this, arguments)
return
ReceiveMessage: ReceiveMessage
)()
// 红队
let player1 = playerFactory('皮蛋', 'red')
let player2 = playerFactory('小乖', 'red')
let player3 = playerFactory('小强', 'red')
let player4 = playerFactory('小雪', 'red')
let player5 = playerFactory('小明', 'red')
<以上是关于JavaScript设计模式与开发实践的主要内容,如果未能解决你的问题,请参考以下文章
学习笔记javascript设计模式与开发实践(职责链模式)----13 http://blog.csdn.net/pigpigpig4587/article/details/50442406#