本文源于本人关于《JavaScript设计模式与开发实践》(曾探著)的阅读总结。想详细了解具体内容建议阅读该书。
- /# :表示重点设计模式
- 原文代码:js-design-mode
1. 策略模式:
定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
前端中的利用:表单验证(不同的表单有不同的验证方式)
一个简单的例子:公司发奖金根据每个人的绩效不同来发不同的奖金,不同的绩效,奖金有不同的计算方式。 我们可以用if-else,判断每个人的绩效是什么,从而采用不同的计算方式。但是如果又增加了一个种绩效水平,那么我们又得增加if-else分支,这明显是违反开放-封闭原则的。
核心思想:创建一个策略组,每次有新的绩效计算方法则直接加入该组里,不会变动其他代码。 调用时,传入绩效字符串,从而采用调用属性的方法访问到正确策略,并调用该策略。
利用策略模式构建奖金发放:
var strategies = {
"S": function(salary) {
return salary * 4;
},
"A": function(salary) {
return salary * 3;
},
"B": function(salary) {
return salary * 2;
}
}
function calculateBonus(level, salary) {
return strategies[level](salary);
}
console.log(calculateBonus('A', 13333));
2. 代理模式:
定义:提供一个代用品或占位符,以便控制对它的访问。
前端中的利用:图片预加载(loading图片)、缓存代理
核心思想:对象A访问对象B,创建一个对象C,控制对象A对对象B的访问,从而达到某种目的。 或者A进行某个行为,创建一个对象C控制A进行的这个行为。
图片预加载:
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
}
}
})()
它返回了一个对象,拥有普通的图片加载功能,但是这个功能有一个弊端,网络环境差,图片迟迟没有完全加载完成时,会产生一个白框,我们希望这个时候有一个loading的动画。
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage.setSrc(this.src);
}
return {
setSrc: function (src) {
myImage.setSrc('./屏幕快照 2017-09-19 上午10.15.58.png');
img.src = src;
}
}
})()
现在创建了一个代理,我们想要加载图片时,并不直接调用图片加载对象,而是调用这个代理函数,达到有loading动画的目的。
它先把imgNode设置为loading动画的gif图片,然后创建了一个Image对象,等传入的真实图片链接,图片加载完成后,再用真实图片替换掉loading动画gif。
当你已经写完了某个函数,但是某时希望这个函数的行为有其他效果时,你就可以写一个代理达到你的目的。
3. 迭代器模式:
定义:提供一种方法顺序访问一个聚合对象中的各个元素。
前端中的利用:循环
很多语言都内置了迭代器,我们很多时候不认为他是一种设计模式。
这里我们说一下外部迭代器:
- 必须显式地请求迭代下一个元素。
- 增加了一些调用的复杂性,但是更为灵活,我们可以手工控制迭代过程和顺序。
var Iterator = function(obj) {
var current = 0;
var next = function() {
current += 1;
};
var isDone = function() {
return current >= obj.length;
};
var getCurrItem = function() {
return obj[current];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem,
length: obj.length
}
};
var compare = function(iterator1, iterator2) {
if(iterator1.length!==iterator2.length) {
console.log('不相等');
}
while(!iterator1.isDone() && !iterator2.isDone()){
if(iterator1.getCurrItem() !== iterator2.getCurrItem()){
console.log('不相等');
}
iterator1.next();
iterator2.next();
}
console.log('相等');
}
compare(Iterator([1, 2, 3]), Iterator([1, 2, 3])); // 相等
4. 命令模式
定义:指的是一个执行某些特定事情的指令。
使用场景:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。
前端中的利用:菜单程序,按键动画
背景:前端协作中,有人负责写界面,有人负责开发按钮之类的具体功能。我们希望写界面的人直接调用命令就好,不用关心,具体实现。
按键动画(每个按键代表不同的动画):
命令创建函数:
var makeCommand = function (receiver, state) {
return function () {
receiver[state]();
}
}
receiver代表具体动画的执行函数。
界面同学只负责:
document.onkeypress = function (ev) {
var keyCode = ev.keyCode,
command = makeCommand(Ryu, commands[keyCode]);
if (command) {
command();
}
};
而实现操作的同学写具体实现,和不同按键所对应的指令名称:
var Ryu = {
attack: function () {
console.log('攻击');
},
defense: function () {
console.log('防御');
},
jump: function () {
console.log('跳跃');
},
crouch: function () {
console.log('下蹲');
}
};
var commands = {
'119': 'jump', // W
'115': 'crouch', // S
'97': 'defense', // A
'100': 'attack' // D
}
目前我们的命令模式,只有一个设置命令,但是这其实完全可以写成一个对象,包含,记录命令调用过程,包含取消命令,等等。
5. 组合模式:
定义:将对象组合成树形结果,以表示“部分-整体”的层次结果。除了用来表示树形结构之外,组合模式令一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
前端中的利用:文件夹扫描
核心思想:树形结构,分为叶子对象和非叶子对象, 叶子对象和非叶子对象拥有一组同样的方法属性, 调用非叶子对象的方法后,该对象和该对象下的所有对象都会执行该方法。
文件扫描:当我们负责粘贴时,我们不会关心我们选中的是文件还是文件夹,我们都会一并进行负责粘贴。
文件夹:
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; file = this.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('JS');
var folder2 = new Folder('JQ');
var file = new File('学习资料');
var file1 = new File('学习资料1');
var file2 = new File('学习资料2');
var file3 = new File('学习资料3');
folder.add(file);
folder.add(file1);
folder1.add(file2);
folder2.add(file3);
var rootFolder = new Folder('root');
rootFolder.add(folder);
rootFolder.add(folder1);
rootFolder.add(folder2);
扫描:
rootFolder.scan();
// 输出:
// 开始扫描文件夹:root
// 开始扫描文件夹:学习资料
// 开始扫描文件:学习资料
// 开始扫描文件:学习资料1
// 开始扫描文件夹:JS
// 开始扫描文件:学习资料2
// 开始扫描文件夹:JQ
// 开始扫描文件:学习资料3
6. 模版方法模式
定义:由两部分结构组成,第一部分就是抽象父类,第二部分就是具体实现的子类。通常父类中封装了子类的算法框架,包括实现一些公共方法及封装子类中所有方法的执行顺序。
使用场景:假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。
模版方法模式所做的事情:我们不必重写一个子类,如果属于同一类型就可以直接继承抽象类,然后把变化的逻辑封装到子类中即可,不需要改动其他子类和父类。
例子:
- 泡咖啡:
- 把水煮沸
- 把沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
- 泡茶:
- 把水煮沸
- 用沸水浸泡茶叶
- 把水倒进杯子里
- 加柠檬
然后进行抽象:
- 把水煮沸
- 用沸水冲泡饮料
- 把饮料倒进杯子里
- 加调料
抽象类代码:
var 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.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
因为JS没有继承机制,但是子类如果继承了父类没有重写方法,编辑器不会提醒,那么执行的时候会报错,为了防止程序员漏重写方法,故在需要重写的方法中抛出异常。
coffee:
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();
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 tea = new Tea();
tea.init();
# 7. 单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
前端中的利用:登录框,弹层
核心思想:利用一个变量保存第一次创建的结果(对象中的某个属性或者闭包能访问的变量), 再次创建时,该变量不为空,直接返回改对象。
类:
var Singleton = function(name) {
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function() {
console.log(this.name);
}
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
var a = Singleton.getInstance('123');
var b = Singleton.getInstance('321');
console.log(a === b); // true
Singleton.getInstance是静态方法。
通用的惰性单例:
function getSingleton(fn) {
var instance = null;
return function() {
return instance || (instance = fn.apply(this, arguments) );
}
}
var createObj = function(name) {
return {name: name};
}
var getSingleObj = getSingleton(createObj);
console.log(getSingleObj('123') === getSingleObj('321'));
fn为实例创建函数,用通用的单例模式包装之后,他就变成了单例创建函数。
# 8. 发布-订阅模式
定义:也可以叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
前端中的利用:Vue双向绑定、事件监听函数。
一个例子-售楼处:
- 很多人登记了信息,当有楼盘的时候,将会通知所有人前来购买。
- 但是每个人的经济能力有限,有些人关注的是别墅楼盘,有些人关注的是小户楼盘,所以每个行为订阅的内容也不一样。
- 有些人嫌这家售楼处的服务态度不好,想取消订阅。
通用实现:创建一个订阅-发布对象,该对象拥有一个客户组对象,拥有订阅方法,发布方法,取消方法。
- 当订阅时:将客户订阅的内容,和执行方法存在客户组对象中:
listen = function (key, fn) {
if (!cacheList[key]) {
cacheList[key] = [];
}
cacheList[key].push(fn);
};
- 取消订阅时:
remove = function (key, fn) {
var fns = cacheList[key];
if (!fns) return false;
// 如果只传了key 代表取消该key下所有客户
if (!fn) {
fns && (fns.length = 0);
} else {
for (var i = fns.length - 1; i >= 0; i--) {
if (fns[i] === fn) {
fns.splice(i, 1);
}
}
}
};
- 发布:
trigger = function () {
var key = Array.prototype.shift.call(arguments),
args = arguments,
fns = cacheList[key];
if (!fns || fns.length === 0) return false;
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, args);
}
}
其实仅仅只有一个客户组时远远不够的,更应该有创建命名空间的功能,详见《JavaScript设计模式与实践》8.11。
# 9. 享元模式
定义:享元模式是一种用于性能优化的模式,核心运用共享技术来支持大量细粒度的对象。
例子:我们有50件男士内衣,和50件女士内衣,我们需要模特穿上拍照。 我们有两种可能性:
- 为50件男士内衣找50个男模特分别拍照 ,为50件女士内衣找50个女模特分别拍照。
- 找一个男模特,和一个女模特,分别穿50次照相。(享元模式)
这个便是享元模式的模型,目的在于减少共享对象的数量,我们需要将对象分为内部状态和外部状态:
- 内部状态存在于对象内部
- 内部状态可以共享
- 内部状态独立与场景,通常不会改变。
- 外部状态决定于场景,根据场景的变化而改变。
上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态来减少系统的对象数量。
前端中的利用:
文件上传:用户选中文件之后,扫码文件后,为每个文件创建一个upload对象,每个upload对象有一个上传类型(插件上传,Flash上传等,不同文件可能适合不同的上传方式),但是如果用户一次性选择的文件太多,则会出现对象过多,对象爆炸。
我们利用以上的方法,分离出外部状态和内部状态。 每个共享对象不变的应该是它的上传类型(内部状态),而改变的是每个上传对象的此时此刻拥有的文件,不同的文件就是外部状态。
创建upload对象:
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);
}
}
})()
定义了一个工厂模式来创建upload对象,如果某种内部状态对应的共享状态已经被创建过,那么直接返回这个对象,否则创建一个新的对象。
管理器封装外部状态:
var uploadManager = (function () {
var uploadDatabase = {};
return {
add: function (id, uploadType, fileName, fileSize) {
var flyWeightObj = UploadFactory.create(uploadType);
var 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, fileSize, dom
};
return flyWeightObj;
},
setExternalState: function (id, flyWeightObj) {
var uploadData = uploadDatabase[id];
for (var i in uploadData) {
flyWeightObj[i] = uploadData[i];
}
}
}
})()
uploadManager对象负责像UploadFactory提交创建对象的请求,并用一个uploadDatabase对象保存upload对象的所有外部状态。
上传函数:
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: 2000,
},
{
fileName: '3.txt',
fileSize: 3000,
}
]);
startUpload('Flash', [
{
fileName: '4.txt',
fileSize: 4000,
},
{
fileName: '5.txt',
fileSize: 5000,
},
{
fileName: '6.txt',
fileSize: 6000,
}
]);
现在不管上传6个文件,还是2000个文件,都只会创建2个对象。
核心思想:
- 创建能共享的对象,每个不同的能共享的对象区别在于内部状态的不同(uploadType)。
- 每个共享的对象依然加上自己的操作,但是在执行操作之前,需要将共享对象指向当前外部状态(文件)。
- 创建一个工厂,能够创建不同内部状态都共享对象,如果该种内部状态的共享对象已经存在,则直接返回。
- 创建一个外部状态管理对象,包含一个数据库对象存储不同外部状态,包含一个添加函数,和指向函数(共享对象指向外部状态)。
# 10. 责任链模式
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着该链传递该请求,直到有一个对象处理它为止。
例子:高峰期公交车,我们不能直接把钱递给售票员,直接给离得比较近的一个人,一直传递下去,最终会到售票员手里。
前端中的利用:
电商网站不同用户种类的下单策略:
- orderType1用户:已经支付500元,得到100元优惠券;未支付500,降级到普通用户购买界面。
- orderType2用户:已经支付200元,得到50元优惠券;未支付200,降级到普通用户购买界面。
- orderType3用户:普通购买。
- 库存限制,针对code3。
新手写法:根据orderType,isPay,stock来写if-else分支来进行具体操作。
责任链模式写法:
分别写order500、order200、orderNormal的函数,如果满足条件则执行,不满足条件则返回一个字段表示交给下一个节点执行:
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('手机库存不足');
}
}
编写责任链控制函数:
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 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);
chainOrder500.passRequest(2, true, 500);
chainOrder500.passRequest(3, true, 500);
chainOrder500.passRequest(1, false, 0);
这样只需要第一个节点执行,如果不满足则请求自动交付给下一个节点,直到到达节点尾部。
如果未来还有更多情况,比如有交了50定金的,可以给10元的优惠券,这样的情况可以直接添加节点,改变节点顺序,不会对已有的方法做更改。
本例子可以用Promise做该写,如果成功则Promise.resolve()否则Promise.reject()
还可以使用AOP的方式Function.prototype.after做改写。
核心思想:将具体执行方法包装为一个个责任链子节点,执行第一个节点,如果情况满足则执行,不满足则调用下一个节点的执行方法。
# 11. 中介者模式
定义:将行为分布到各个对象中,把对象划分为更小的细粒度,但是由于细粒度之间对象的联系激增,又有可能反过来降低它们的可复用性。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
例子:
- 机场指挥中心:每架飞机不可能和其他所有飞机逐一联系,来确定是否能起飞,是否能滑动,这样的联系都交给了指挥中心来做。每架飞机只需要联系中介者即可。
- 博彩公司算赔率:和机场指挥中心是一样的道理。
前端的利用:
商品购买:通常商品购买会有选择框,输入框,还有信息提示框,我们需要选择或者输入时,信息都能有正确的提示,一个办法是强耦合,在选择框变动后,去修改提示框。如果添加新的选择框,代码变动会更大。
引入中介者:具体处理逻辑交给中介者处理,其他选择框只与中介者交互。
html:
<body>
选择颜色:
<select name="" id="colorSelect">
<option value="">请选择</option>
<option value="red">红色</option>
<option value="blue">蓝色</option>
</select>
<br> 选择内存:
<select name="" id="memorySelect">
<option value="">请选择</option>
<option value="32G">32g</option>
<option value="16G">16g</option>
</select>
<br>
<br> 输入购买数量:
<input type="text" id="numberInput">
<br>
<br> 您选择了颜色:
<div id="colorInfo"></div>
<br> 您选择了内存:
<div id="memoryInfo"></div>
<br> 您输入了数量:
<div id="numberInfo"></div>
<br>
<button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button>
</body>
获取各种框dom节点:
var colorSelect = document.getElementById('colorSelect');
var memorySelect = document.getElementById('memorySelect');
var numberInput = document.getElementById('numberInput');
var colorInfo = document.getElementById('colorInfo');
var memoryInfo = document.getElementById('memoryInfo');
var numberInfo = document.getElementById('numberInfo');
var nextBtn = document.getElementById('nextBtn');
编写中介者:
var mediator = (function () {
return {
changed: function (obj) {
var color = colorSelect.value,
memory = memorySelect.value,
number = numberInput.value,
stock = goods[color + '|' + memory];
if (obj === colorSelect) {
colorInfo.innerHTML = color;
} else if (obj === memorySelect) {
memoryInfo.innerHTML = memory;
} else if (obj === numberInput) {
numberInfo.innerHTML = number;
}
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return;
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存大小';
return;
}
if (!(Number.isInteger(number - 0) && number > 0)) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购买数量';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
}
})();
变动只与中介者交互:
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numberInput.oninput = function() {
mediator.changed(this);
};
12. 装饰者模式
定义:在不改变对象自身的基础上,在程序运行期间给对象动态添加职责。(包装器)
例子:
- 给自行车扩展,给4种自行车扩展3个配件,在继承的基础上需要建立出12个子类。
- 但是动态的把这些动态添加到自行车上则住需要额外3个类(3个配件)。
装饰者:
// 保存引用的装饰者模式
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();
AtomDecorator 包装 MissileDecorator 包装 Plane。 这样写完全符合开发-封闭原则,在添加新功能的时候没有去改动别人点方法,但是不好的就是,如果包装点层次太多,中间变量就太多了。还会遇见this劫持的问题:
var _getEleById = document.getElementById;
document.getElementById = function(id) {
alert(1);
return _getElementById(id);
}
this被劫持了。
解决以上两个问题的最好方法就上AOP函数:
Function.prototype.before = function (fn) {
var _self = this; // 保存原函数的引用
return function () { // 返回了包含原函数和新函数的代理函数
fn.apply(this, arguments);
return _self.apply(this, arguments); // 执行原函数
}
}
Function.prototype.after = function (fn) {
var _self = this; // 保存原函数的引用
return function () {
var ret = _self.apply(this, arguments);
fn.apply(this, arguments);
return ret;
}
}
- 第一个:返回在函数之前执行
- 第二个:返回在函数之后执行
前端的利用:数据上报这样和业务逻辑无关的函数都可以利用包装者进行包装。
# 13. 状态模式:
定义:区分事物的内部状态,事物的内部状态的改变往往会带来事物行为的改变。
例子:
- 通常的电灯,只有一个按钮,按下按钮;
- 如果电灯是关的:那么开灯
- 如果点灯是开着的:那么关灯
这里换成代码,就是简单的if-else,但是如果再复杂一点呢:新添加一个按钮,如果这个按钮按下,那么点灯是弱-强-关模式;否则是开-关模式。
这个时候你已经开始发现if-else代码的缺点了:
- 每次灯扩展,都需要修改内部代码,违反开放-封闭原则
- 所有与行为有关的事情都在一个函数里
- 状态切换不明显,仅仅只有一个字段的改变
- if-else太多太繁杂。
状态模式下的点灯程序(假设这里只有一个按钮,切换开关):
我们第一步创建点灯(富含状态的这个对象):
var Light = function () {
this.currState = FSM.off;
this.button = null;
};
this.currState代表的是不同的状态:这里的状态用对象来表示,开关两个状态就是两个对象:
var FSM = {
off: {
buttonWasPressed: function () {
console.log('关灯');
this.button.innerHTML = '下一次按我是开灯';
this.currState = FSM.on;
}
},
on: {
buttonWasPressed: function() {
console.log('开灯');
this.button.innerHTML = '下一次按我是关灯';
this.currState = FSM.off;
}
}
}
接下来编写初始化电灯函数:
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.call(self);
}
};
给按钮绑定事件,按钮触发时,触发当前状态对象的更替事件。(执行外部行为,切换当前的状态)
总结:
状态模式编写思路:
- 设计富含状态的对象(主对象):
- 编写各种状态下的行为
- 状态属性
- 初始化:绑定按钮,在该状态下的状态切换
- 设计各种状态对象:
- 接收主对象的this
- 按钮触发时,改状态利用this,多状态则编写多个不同触发函数
切换主对象状态、调用主状态行为
与策略模式的区别:
- 策略模式中每个策略类相互平等没有关系
- 状态模式中状态类之间的关系是提前确定好的。
14. 适配器模式
定义:解决两个软件实体之间的接口不兼容的问题。
例子:插头转换器,转换不同地区的电压问题。
前端中:
- 地图渲染:
假如地图渲染的函数是这样的:
var renderMap = function( map ) {
if(map.show instanceof Function) {
map.show();
}
}
地图:
var googleMap = {
show() {
console.log('google地图开始渲染');
}
}
var baiduMap = {
display() {
console.log('baidu地图开始渲染');
}
}
我们可以只带googleMap没有问题,但是baiduMap提供的接口名明显不一致,如果去改renderMap函数违反了开放封闭原则。
那么现在我们只能用适配器包装一下baiduMap:
var baiduMapAdapter = {
show() {
return baiduMap.display();
}
}
思路:封装与其他不同的方法或者对象,而不要去改动原有的函数。
其他例子:xml与json格式适配,json与对象格式的转变等。