mhahaha / javascript-design-patterns Goto Github PK
View Code? Open in Web Editor NEWJavaScript设计模式学习总结
Home Page: https://github.com/mhahaha/Javascript-Design-Patterns
JavaScript设计模式学习总结
Home Page: https://github.com/mhahaha/Javascript-Design-Patterns
“组合”,顾名思义即多个小个体或者小对象构成的“大对象”,构成组合的小对象本身也可以是更小的对象。
const closeDoorCommand = {
execute: () => {
console.log('关门');
}
}
const openPcCommand = {
execute: () => {
console.log('开电脑');
}
}
const openQQCommand = {
execute: () => {
console.log('登录 QQ');
}
}
class MacroCommand {
constructor () {
this.commandsList = []
}
add (command) {
this.commandsList.push(command)
}
execute () {
for (let i = 0, command; command = this.commandsList[i++];) {
command.execute()
}
}
}
const macroCommand = new MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute()
观察代码可以看出,MacroCommand其实是由['closeDoorCommand', 'openPcCommand', 'openQQCommand']三个命令组成的一个命令集,即宏命令(MacroCommand)中包含了一组子命令,组成了一个简单的树结构,marcoCommand就是组合对象,closeDoorCommand、openPcCommand、openQQCommand 都是叶对象。在 macroCommand 的 execute 方法里,并不执行真正的操作,而是遍历它所包含的对象,把真正的 execute 请求委托给这些叶对象。macroCommand 表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但 macroCommand 只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问。
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态表现,使得用户对单个对象和组合对象的使用具有一致性。
上面例子可以看出,我们在添加功能命令时,我们并不会关心这个命令是普通的子命令还是组合了多种功能的宏命令,只要具有execute方法,我们都可以添加成功,且可以自由组合,在实际开发中,拓展性时非常好的。所有我们通常可以用组合模式来完成类似这类“多个功能自由组合”的开发需求。
我们上面提到的是多个子命令组合成宏命令,也说到宏命令子命令可以“自由组合”,那我们就上面的示例进行一个小小的拓展来验证下我们刚才的总结吧
const closeDoorCommand = {
execute: () => {
console.log('关门');
}
}
const openPcCommand = {
execute: () => {
console.log('开电脑');
}
}
const openQQCommand = {
execute: () => {
console.log('登录 QQ');
}
}
class MacroCommand {
constructor (name) {
this.name = name
this.commandsList = []
}
add (command) {
this.commandsList.push(command)
}
execute () {
console.log(`${this.name}:`)
for (let i = 0, command; command = this.commandsList[i++];) {
command.execute()
}
}
}
const macroCommand1 = new MacroCommand('命令1')
macroCommand1.add(closeDoorCommand)
macroCommand1.add(openPcCommand)
macroCommand1.execute()
const macroCommand2 = new MacroCommand('命令2')
macroCommand2.add(openPcCommand)
macroCommand2.add(openQQCommand)
macroCommand2.execute()
// 组合宏命令
const macroCommand3 = new MacroCommand('命令3')
macroCommand3.add(macroCommand1)
macroCommand3.add(macroCommand2)
macroCommand3.execute()
执行以上代码,结果完全符合我们预期,无论组合多么复杂的层级,组合模式都可以不断递归下去。
组合模式最大的优点在于可以一致地对待组合对象和基本对象,它的透明性使得发起请求的用户不用去顾虑树中组合对象和叶对象的区别,但它们在本质上有是区别的,组合对象可以拥有子节点,叶对象下面就没有子节点, 所以我们也许会发生一些误操作,比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加 add 方法,并且在调用这个方法时,抛出一个异常来及时提醒用户
const openQQCommand = {
execute: () => {
console.log('登录 QQ');
},
add: () => {
throw new Error( '叶对象不能添加子节点' )
}
}
组合模式不是父子关系
组合模式是一种 HAS-A(聚合)的关系,而不是 IS-A。它们能够合作的关键是拥有相同的接口。
对叶对象操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性。
组合模式虽然好,但是我们实际开发时并不是所有场景都适用,只有深入理解才可以在一些场景里面很快想到设计模式的使用,终归是想让自己的代码更好理解,可读性更高,感觉设计模式这东西真的是需要反复地看反复地琢磨>_<,希望自己可以很好地坚持。
最初看到“模板方法模式“,感觉应该就是一个模板,支持不同的设置。看了书上的解释觉得理解的差不多,就是把相同的剥离出来作为模板(父类),不同的设置放在模板以外(子类)。
模板方法模式 = 抽象父类 + 具象子类
先通过一个例子加深对模板方法模式的理解。
Coffee or Tea
泡一杯咖啡步骤:
1.把水煮沸
2.用沸水冲咖啡
3.把咖啡倒进杯子
4.加糖和牛奶
泡一壶茶步骤:
1.把水煮沸
2.用沸水浸泡茶叶
3.把茶水倒进杯子
4.加柠檬
按照我们上面的理解,我们首先应该做的是找出泡咖啡和泡茶这两个过程的相同点和不同点。
我们可以找出以下不同点:
- 原料不同:茶 or 咖啡
- 泡的方式不同:冲泡 or 浸泡
- 加的调料不同:糖和牛奶 or 柠檬
现在大概可以构思出一个模板方法的大概的框架:
1.把水煮沸 (boilWater)
2.用沸水冲饮料 (brew)
3.把饮料倒进杯子 (pourInCup)
4.加调料 (addCondiments)
我们先用function来实现该框架第一版:
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()
}
上面 init
方法就是我们模板方法模式中的 模板方法
,因为它封装了子类的算法框架,作为一个算法模板来执行一系列方法。
现在我们有了模板父类,我们现在缺具象的实现子类。接下来我们先创建Coffee和Tea子类:
// 创建咖啡子类
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('加糖和牛奶')
}
// 创建Tea子类
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 coffee = new Coffee()
coffee.init()
var tea = new Tea()
tea.init()
上面的示例只是为了更好的理解模板方法模式,换在现在的一个需求场景我们可能不会这么去做,可能简单的回调或者是参数判断我们就可以简单实现,而并不是一定要用这种继承方式。
var Beverage = function (param = {}) {
var boilWater = function () {
console.log('把水煮沸')
}
// 考虑代码的健壮性,当然还得校验参数的是function类型,这里就略过啦
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 tempFunction = function () {}
tempFunction.prototype.init = function () {
boilWater()
brew()
pourInCup()
addCondiments()
}
return tempFunction
}
var Coffee = Beverage({
brew: function () {
console.log('用水泡咖啡')
},
pourInCup: function () {
console.log('将咖啡倒进杯子')
},
addCondiments: function () {
console.log('加糖和牛奶')
}
})
var coffee = new Coffee()
coffee.init()
Tea照葫芦画瓢...
上面实现的可以实现同样的效果,模板内部不需要关心子类有没有相应的处理方法,子类负责一些细节处理就好。这里引入了一个新的设计原则——“好莱坞原则” 可以穿插了解一下。
“不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞原则。
类似的,我们用ES6的class也可以实现,基础示例代码如下:
class Beverage {
constructor (param = {}) {
this.param = param
}
boilWater () {
console.log('把水煮沸')
}
brew () {
(this.param.brew || function () {
throw new Error('必须传递brew方法')
})()
}
pourInCup () {
(this.param.pourInCup || function () {
throw new Error('必须传递pourInCup方法')
})()
}
addCondiments() {
(this.param.addCondiments || function () {
throw new Error('必须传递addCondiments方法')
})()
}
init () {
this.boilWater()
this.brew()
this.pourInCup()
this.addCondiments()
}
}
const coffee = new Beverage ({
brew: function () {
console.log('用沸水泡咖啡')
},
pourInCup: () => {
console.log('将咖啡倒入杯子')
},
addCondiments: () => {
console.log('加块冰')
}
})
coffee.init()
进一步说明了设计模式中从分析逻辑中不变的
和变化的
的同时可以带给我们很好的优化空间,很受用。只有充分的理解了模板方法模式的定义我们才可以更好的去使用它,在合适的场景联想到它。我感觉去做功能框架的时候,真正用得上的话可以省好多重复逻辑的代码哦,想一下项目中哪里可以进行相关优化......
俗话说,条条大路通罗马,完成一件事情,我们往往有很多选择,根据自身不同的条件我们可以有不同的选择方案。比如我们要去某个地方旅游,可以根据具体的实际情况来选择出行的线路。
这类选择过程也即是我们即将学习的策略模式。
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
示例:编写一个计算奖金(bonus)的算法,薪资(salary) 为最小单元
可以看出,计算出员工奖金,我们需要知道员工的级别及薪资,我们可以编写一个方法,将级别和薪资当做参数传进去,这样我们就可以得到员工奖金。如下:
const calculateBonus = (performanceLevel, salary) => {
if (performanceLevel === 'S') {
return salary * 3
}
if (performanceLevel === 'A') {
return salary * 2
}
if (performanceLevel === 'B') {
return salary * 1
}
}
calculateBonus('S', 8000)
可以发现,这段代码十分简单,但是存在着显而易见的缺点。
在javascript中,声明一个对象的成本是很低的,我们根据策略模式的定义来改造下,上面计算函数我们完全可以简化成一个对象来进行维护,key是级别,value则是一个计算奖金的函数,代码如下:
const strategies = {
'S': salary => salary * 3,
'A': salary => salary * 2,
'B': salary => salary * 1
};
const calculateBonus = (level, salary) => strategies[ level ]( salary )
calculateBonus( 'S', 20000 )
这样一来,我们消除了大片的if-esle条件分支语句,使实现过程更加简洁,只需维护好strategies这个策略对象即可。
以上示例我们可以了解到策略模式的基本使用,那在我们实际开发过程,其实很多地方都可以用策略模式来进行改进,比如我们前端的一些校验,比较急躁的时候可能会出现上面庞大的if-else来进行各类验证条件的编写,那现在我们用策略模式来改进试试看,实现代码如下:
// 策略对象
const strategies = {
isNonEmpty: (value, errorMsg) => {
if (value === '') {
return errorMsg
}
},
minLength: (value, length, errorMsg) => {
if (value.length < length) {
return errorMsg
}
},
isMobile: (value, errorMsg) => {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg
}
}
}
// 处理校验的校验类
class Validator {
constructor() {
this.cache = []
}
add (ele, rules) {
for (let i = 0, rule; (rule = rules[i++]);) {
const strategyAry = rule.strategy.split(':')
const errorMsg = rule.errorMsg
this.cache.push(() => {
const strategy = strategyAry.shift()
strategyAry.unshift(ele.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(ele, strategyAry)
})
}
}
start () {
for (let i = 0, validatorFunc; (validatorFunc = this.cache[i++]);) {
const errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// demo
const registerForm = {
userName: {
value: 'lalalalala'
},
password: {
value: 'mimamima'
},
phoneNumber: {
value: 13123456789
}
}
const validataFunc = () => {
const validator = new Validator()
validator.add(registerForm.userName, [
{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
},
{
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 6 位'
}
]);
validator.add(registerForm.password, [
{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于 6 位'
}
]);
validator.add(registerForm.phoneNumber, [
{
strategy: 'isMobile',
errorMsg: '手机号码格式不正确'
}
])
const errorMsg = validator.start() // 开始校验,并取得校验后的返回信息
return errorMsg
}
const errorMsg = validataFunc()
if (errorMsg) {
console.log(errorMsg)
}
这样我们按照策略模式新增并维护一个策略类,然后按照规则添加各数据项的校验就好,保证了功能代码可读性及扩展性。
实际开发过程,当发现一个方法逐渐变得庞大,if-else写得让你难受的时候,试试策略模式改造能否解决痛点。
单例模式定义: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
适用场景:全局只需要一个的对象时,例如线程池,全局缓存等
原理:获取一个实例时先判断该实例是否创建,若未创建,则先创建并缓存,然后返回实例;若已创建,则返回缓存中的实例。
示例代码:
const userInfo = null
const getUserInfo = (name, info) => {
if (!userInfo) {
userInfo = {
name: name,
info: info
}
}
return userInfo
}
当使用这种方式创建一个仅有的实例时,我们是在全局作用域中新增全局变量,存在很多全局变量时,很容易造成变量污染,所以我们要么适当使用命名空间来控制全局变量数量,例如:
const userInfo = null
const namespace1 = {
a: 1,
b: 2
}
要么使用闭包来封装私有变量,仅暴露跟外界通信的接口:
const user = (() => {
const name = 'dev'
const age = 20
return {
getUserInfo: () => {
return `${name} - ${age}`
}
}
})()
惰性单例在我们实际开发过程是很常用也很重要的技术,例如我们页面初始化时创建登录弹窗,我们常见的做法可能有以下几种:
1.静态直出直接添加弹窗,控制其显示及隐藏
2.需要登录时初始化,退出时remove
3.需要登录时初始化,退出时hide
可以比较三者的消耗,第三种显然是最合理的,我们进入一个页面只是想看一些附加功能,可能并不需要登录,也可能我们登录之后需要退出登录进行账号的切换...
惰性单例的核心就是对象被调用时才会被真正创建。
开发过程中,我们发现有一段代码可以复用时,我们当然是想直接copy过来,但此时如果让我们去熟悉里面的逻辑并做部分修改,我个人是比较排斥的。同样的道理,写单例模式时,我们也要尽量做到其通用性。
通用惰性单例 = 改变的业务逻辑 + 不变的单例模式框架
const getSingleton = fn => {
let result
return () => {
return result || (result = fn.apply(this, argument))
}
}
此时,任何业务都可以以参数的形式进行传递来完成一次单例模式的使用,例如:
const createLoginIframe = () => {
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe
}
const loginIframe = getSingleton(createLoginIframe)
在这个例子中可以看出,职责比较分明,创建实例对象的职责和管理单例的职责放置在两个方法里面,没有业务耦合,完成了单例模式的通用性。
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
###实现自己的迭代器
现在我们来自己实现一个 each 函数,each 函数接受 2 个参数,第一个为被循环的数组,第二个为循环中的每一步后将被触发的回调函数:
const each = (ary, callback) => {
for (let i = 0, l = ary.length; i < l; i++) {
// 把下标和元素当作参数传给 callback 函数
callback.call(ary[i], i, ary[i])
}
}
each([1, 2, 3], (i, n) => {
alert([i, n])
})
迭代器可以分为内部迭代器和外部迭代器,它们有各自的适用场景。这一节我们将分别讨论这两种迭代器。
上面刚写的each就是内部迭代器,它内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。这也暴露出内部迭代器的缺点,即只能按照迭代器内部的规则进行迭代,例如each不能同时迭代两个数组
以“判断两数组是否相等”这个需求为例,加深对内部迭代器与外部迭代器的理解。
【内部迭代器实现】
const compare = (ary1, ary2) => {
if (ary1.length !== ary2.length) {
throw new Error('ary1 和 ary2 不相等')
}
each(ary1, (i, n) => {
if (n !== ary2[i]) {
throw new Error('ary1 和 ary2 不相等')
}
})
console.log('ary1 和 ary2 相等')
}
compare([1, 2, 3], [1, 2, 4]) // throw new Error ( 'ary1 和 ary2 不相等' )
【外部迭代器实现】
const Iterator = obj => {
let current = 0
const next = () => {
current++
}
const isDone = () => current >= obj.length
const getCurrItem = () => obj[current]
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem
}
}
const compare = (iterator1, iterator2) => {
while (!iterator1.isDone() && !iterator2.isDone()) {
if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
throw new Error('iterator1 和 iterator2 不相等')
}
iterator1.next()
iterator2.next()
}
console.log('iterator1 和 iterator2 相等')
}
const iterator1 = Iterator([1, 2, 3])
const iterator2 = Iterator([1, 2, 3])
compare(iterator1, iterator2) // 输出:iterator1 和 iterator2 相等
外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。内部迭代
器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定。
###迭代类数组对象和字面量对象
迭代器模式不仅可以迭代数组,还可以迭代一些类数组的对象。比如 arguments、{"0":'a',"1":'b'}等。通过上面的代码可以观察到,无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代。在 JavaScript 中,for in 语句可以用来迭代普通字面量对象的属性。jQuery 中提供了$.each`函数来封装各种迭代行为:
$.each = (obj, callback) => {
let value,
i = 0
const length = obj.length
const isArray = isArraylike(obj)
if (isArray) { // 迭代类数组
for (; i < length; i++) {
value = callback.call(obj[i], i, obj[i])
if (value === false) {
break
}
}
} else {
for (i in obj) { // 迭代 object 对象
value = callback.call(obj[i], i, obj[i])
if (value === false) {
break
}
}
}
return obj
}
###倒序迭代器
迭代器模式提供了循环访问一个聚合对象中每个元素的方法,但它没有规定我们以顺序、倒序还是中序来循环遍历聚合对象。下面我们实现一个倒序访问的迭代器:
const reverseEach = (ary, callback) => {
for (const l = ary.length - 1; l >= 0; l--) {
callback(l, ary[l])
}
}
reverseEach([0, 1, 2], (i, n) => {
console.log(n) // 分别输出:2, 1 ,0
})
迭代器可以像普通 for 循环中的 break 一样,提供一种跳出循环的方法。
if (value === false) {
break
}
约定如果回调函数的执行结果返回 false,则提前终止循环,终止迭代器示例如下:
const each = (ary, callback) => {
for (let i = 0, l = ary.length; i < l; i++) {
if (callback(i, ary[i]) === false) { // callback 的执行结果返回 false,提前终止迭代
break
}
}
}
each([1, 2, 3, 4, 5], (i, n) => {
if (n > 3) { // n 大于 3 的时候终止循环
return false
}
console.log(n) // 分别输出:1, 2, 3
})
###迭代器模式的应用举例
【根据不同的浏览器获取相应的上传组件对象】
在不同的浏览器环境下,选择的上传方式是不一样的。因为使用浏览器的上传控件进行上传速度快,可以暂停和续传,所以我们首先会优先使用控件上传。如果浏览器没有安装上传控件,则使用 Flash 上传, 如果连 Flash 也没安装,那就只好使用浏览器原生的表单上传了。
const getUploadObj = () => {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
} catch (e) {
if (supportFlash()) { // supportFlash 函数未提供
const str = '<object type="application/x-shockwave-flash"></object>'
return $(str).appendTo($('body'))
} else {
const str = '<input name="file" type="file"/>' // 表单上传
return $(str).appendTo($('body'))
}
}
}
以上代码虽然可以实现该需求,但是存在很明显的缺陷:
后续还有其他上传方案的话只有继续添加if-else分支,比较难看。
下面我们使用迭代器模式来实现上面需求,代码如下:
// 定义各类上传方案的初始化类
const getActiveUploadObj = () => {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload") // IE 上传控件
} catch (e) {
return false
}
}
const getFlashUploadObj = () => {
if (supportFlash()) { // supportFlash 函数未提供
const str = '<object type="application/x-shockwave-flash"></object>'
return $(str).appendTo($('body'))
}
return false
}
const getFormUpladObj = () => {
const str = '<input name="file" type="file" class="ui-file"/>' // 表单上传
return $(str).appendTo($('body'))
}
// 迭代器使得定义的上传组件可以按照优先级进行循环迭代
// 迭代器内部循环过程假如返回一对象,则执行对应的方法,反之则返回false继续执行
const iteratorUploadObj = () => {
for (let i = 0, fn; fn = arguments[i++];) {
const uploadObj = fn()
if (uploadObj !== false) {
return uploadObj
}
}
}
const uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUpladObj)
很显然,迭代器模式中,功能职责的分配比较清晰,也比较容易理解,可以很方便地的维护和扩展。
可以看出,合理运用迭代器模式可以使我们在开发过程很清楚地规划代码逻辑,使我们编写的代码拥有很好的维护性和拓展性,实现成本也比较低,还是比较简单,很容易理解的,更何况现在包括JavaScript在内的大部分语言都内置迭代器,我们只需遵循一定的规则去使用就好了。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.