现在,我们终于步入了设计模式学习的殿堂。
在将函数作为一等对象的语言中,有许多需要利用对象多态性的设计模式,比如命令模式、
策略模式等,这些模式的结构与传统面向对象语言的结构大相径庭,实际上已经融入到了语言之
中,我们可能经常使用它们,只是不知道它们的名字而已。
第二部分并没有全部涵盖 GoF 所提出的 23 种设计模式,而是选择了在 JavaScript 开发中更
常见的 14 种设计模式
单例模式
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏
览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我
们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少
次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建
实现单例模式
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建
过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。代码如下:
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
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。
var a = {};
当用这种方式创建对象 a 时,对象 a 确实是独一无二的。如果 a 变量被声明在全局作用域下,
则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就
满足了单例模式的两个条件。
但是全局变量存在很多问题,它很容易造成命名空间污染。在大中型项目中,如果不加以限
制和管理,程序中可能存在很多这样的变量。JavaScript 中的变量也很容易被不小心覆盖,相信
每个 JavaScript 程序员都曾经历过变量冲突的痛苦,就像上面的对象 var a = {};,随时有可能被
别人覆盖。
作为普通的开发者,我们有必要尽量减少全局变量的使用,即使需要,也要把它的污染降到
最低。以下几种方式可以相对降低全局变量带来的命名污染。
- 使用命名空间
var MyApp = {};
MyApp.namespace = function( name ){
var parts = name.split( '.' );
var current = MyApp;
for ( var i in parts ){
if ( !current[ parts[ i ] ] ){
current[ parts[ i ] ] = {};
}
current = current[ parts[ i ] ];
MyApp.namespace( 'event' );
MyApp.namespace( 'dom.style' );
console.dir( MyApp );
// 上述代码等价于:
var MyApp = {
event: {},
dom: {
style: {}
}
};
- 使用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
var user = (function(){
var __name = 'sven',
__age = 29;
return {
getUserInfo: function(){
return __name + '-' + __age;
}
}
})();
我们用下划线来约定私有变量__name 和__age,它们被封装在闭包产生的作用域中,外部是
访问不到这两个变量的,这就避免了对全局的命令污染。
惰性单例
**惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点&&,这种技术在实
际开发中非常有用,有用的程度可能超出了我们的想象,
作者给我们举了个例子
假设我们是 WebQQ 的开发人员(网址是web.qq.com),当点击左边导航里 QQ 头像时,会弹
出一个登录浮窗(如图 4-1 所示),很明显这个浮窗在页面里总是唯一的,不可能出现同时存在
两个登录窗口的情况
- 用户点击登录按钮的时候才开始创建该浮窗
<html>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
var createLoginLayer = function(){
var 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';
};
</script>
</html>
虽然现在达到了惰性的目的,但失去了单例的效果。当我们每次点击登录按钮的时候,都会
创建一个新的登录浮窗 div。虽然我们可以在点击浮窗上的关闭按钮时(此处未实现)把这个浮
窗从页面中删除掉,但这样频繁地创建和删除节点明显是不合理的,也是不必要的。
- 我们可以用一个变量来判断是否已经创建过登录浮窗
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';
};
通用的惰性单例
这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在 createLoginLayer
对象内部。
如果我们下次需要创建页面中唯一的 iframe,或者 script 标签,用来跨域请求数据,就
必须得如法炮制,把 createLoginLayer 函数几乎照抄一遍
我们需要把不变的部分隔离出来,先不考虑创建一个 div 和创建一个 iframe 有多少差异,管
理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建
过对象,如果是,则在下次直接返回这个已经创建好的对象
现在我们就把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在 getSingle
函数内部,创建对象的方法 fn 被当成参数动态传入 getSingle 函数
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';
};
在 getSinge 函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实
用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的
是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模
式的威力
策略模式
很多时候也有多种途径到达同一个目的地。比如我们要去某个地方旅游,
可以根据具体的实际情况来选择出行的线路。
如果没有时间但是不在乎钱,可以选择坐飞机。
如果没有钱,可以选择坐大巴或者火车。
如果再穷一点,可以选择骑自行车。
在程序设计中,我们也常常遇到类似的情况,要实现某一个功能有多种方案可以选择
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换
使用策略模式计算奖金
很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年
终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财
务部要求我们提供一段代码,来方便他们计算员工的年终奖
- 最初的代码实现
var calculateBonus = function( performanceLevel, salary ){
if ( performanceLevel === 'S' ){
return salary * 4;
}
if ( performanceLevel === 'A' ){
return salary * 3;
}
if ( performanceLevel === 'B' ){
return salary * 2;
}
};
calculateBonus( 'B', 20000 ); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000
可以发现,这段代码十分简单,但是存在着显而易见的缺点。
calculateBonus 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑
分支。
calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金
系数改为 5,那我们必须深入 calculateBonus 函数的内部实现,这是违反开放封闭原则的。
算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择
只有复制和粘贴。
因此,我们需要重构这段代码
- 使用组合函数重构代码
var performanceS = function( salary ){
return salary * 4;
};
var performanceA = function( salary ){
return salary * 3;
};
var performanceB = function( salary ){
return salary * 2;
};
var calculateBonus = function( performanceLevel, salary ){
if ( performanceLevel === 'S' ){
return performanceS( salary );
}
if ( performanceLevel === 'A' ){
return performanceA( salary );
}
if ( performanceLevel === 'B' ){
return performanceB( salary );
}
};
calculateBonus( 'A' , 10000 ); // 输出:30000
目前,我们的程序得到了一定的改善,但这种改善非常有限,我们依然没有解决最重要的问
题:calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。
- 使用策略模式重构代码
策略模式指的是定义一系
列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策
略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来
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;
};
接下来定义奖金类 Bonus:
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
刚刚我们用策略模式重构了这段计算年终奖的代码,可以看到通过策略模式重构之后,代码
变得更加清晰,各个类的职责更加鲜明。但这段代码是基于传统面向对象语言的模仿
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
策略模式的优缺点
策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它
们易于切换,易于理解,易于扩展。
策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻
便的替代方案
当然,策略模式也有一些缺点,但这些缺点并不严重。
首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的
逻辑堆砌在 Context 中要好
代理模式
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身
对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之
后,再把请求转交给本体对象。
例子——小明追 MM 的故事
四月一个晴朗的早晨,小明遇见了他的百分百女孩,我们暂且称呼小明的女神为
A。两天之后,小明决定给 A 送一束花来表白。刚好小明打听到 A 和他有一个共同的朋友 B,于是内向的小明决定让 B 来代替自己完成送花这件事情
A 的朋友 B 却很了解 A,所以小明只管把花交给 B,B 会监听 A 的心情变化,然后选
择 A 心情好的时候把花转交给 A
var Flower = function(){};
var xiaoming = {
sendFlower: function( target){
var flower = new Flower();
target.receiveFlower( flower );
}
};
var B = {
receiveFlower: function( flower ){
A.listenGoodMood(function(){ // 监听 A 的好心情
A.receiveFlower( flower );
});
}
};
var A = {
receiveFlower: function( flower ){
console.log( '收到花 ' + flower );
},
listenGoodMood: function( fn ){
setTimeout(function(){ // 假设 10 秒之后 A 的心情变好
fn();
}, 10000 );
}
};
xiaoming.sendFlower( B );
虚拟代理合并 HTTP 请求
vue2 中的watcher队列更新用到了这种模式
假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同
步到另外一台备用服务器上面当我们选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。而点击一个
checkbox 并不是很复杂的操作,作为 APM250+的资深 Dota 玩家,我有把握一秒钟之内点中 4 个
checkbox。可以预见,如此频繁的网络请求将会带来相当大的开销。
解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,
最后一次性发送给服务器。比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给
服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大副作用,却能大大减轻
服务器的压力。
var synchronousFile = function( id ){
console.log( '开始同步文件,id 为: ' + id );
};
var proxySynchronousFile = (function(){
var cache = [], // 保存一段时间内需要同步的 ID
timer; // 定时器
return function( id ){
cache.push( id );
if ( timer ){ // 保证不会覆盖已经启动的定时器
return;
}
timer = setTimeout(function(){
synchronousFile( cache.join( ',' ) ); // 2 秒后向本体发送需要同步的 ID 集合
clearTimeout( timer ); // 清空定时器
timer = null;
cache.length = 0; // 清空 ID 集合
}, 2000 );
}
})();
var checkbox = document.getElementsByTagName( 'input' );
for ( var i = 0, c; c = checkbox[ i++ ]; ){
c.onclick = function(){
if ( this.checked === true ){
proxySynchronousFile( this.id );
}
}
};
缓存代理
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
迭代器模式
迭代器模式无非就是循环访问聚合对象中的各个元素。
内部迭代器和外部迭代器
迭代器可以分为内部迭代器和外部迭代器
- 内部迭代器
内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅
仅是一次初始调用,但这也刚好是内部迭代器的缺点。由于内部迭代器的迭代规则已经被提前规
定,上面的 each 函数就无法同时迭代 2 个数组了。
var each = function( ary, callback ){
for ( var i = 0, l = ary.length; i < l; i++ ){
callback.call( ary[i], i, ary[ i ] ); // 把下标和元素当作参数传给 callback 函数
}
};
each( [ 1, 2, 3 ], function( i, n ){
alert ( [ i, n ] );
});
- 外部迭代器
外部迭代器必须显式地请求迭代下一个元素
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
}
};
// 再看看如何改写 compare 函数:
var compare = function( iterator1, iterator2 ){
while( !iterator1.isDone() && !iterator2.isDone() ){
if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){
throw new Error ( 'iterator1 和 iterator2 不相等' );
}
iterator1.next();
iterator2.next();
}
alert ( 'iterator1 和 iterator2 相等' );
}
var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] );
compare( iterator1, iterator2 ); // 输出:iterator1 和 iterator2 相等
外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。内部迭代
器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定
发布—订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状
态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型
来替代传统的发布—订阅模式。
现实中的发布-订阅模式
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼
MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。
但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除
了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决
定辞职,因为厌倦了每天回答 1000 个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在
了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一
样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册,
遍历上面的电话号码,依次发送一条短信来通知他们
var event = {
clientList: [],
listen: function( key, fn ){
if ( !this.clientList[ key ] ){
this.clientList[ key ] = [];
}
this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表
},
trigger: function(){
var key = Array.prototype.shift.call( arguments ), // (1);
fns = this.clientList[ key ];
if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
return false;
}
for( var i = 0, fn; fn = fns[ i++ ]; ){
fn.apply( this, arguments ); // (2) // arguments 是 trigger 时带上的参数
}
}
};
// 再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能:
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
};
// 再来测试一番,我们给售楼处对象 salesOffices 动态增加发布—订阅功能:
var salesOffices = {};
installEvent( salesOffices );
salesOffices.listen( 'squareMeter88', function( price ){ // 小明订阅消息
console.log( '价格= ' + price );
});
salesOffices.listen( 'squareMeter100', function( price ){ // 小红订阅消息
console.log( '价格= ' + price );
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000
salesOffices.trigger( 'squareMeter100', 3000000 ); // 输出:3000000
发布—订阅模式的优点非常明显:
- 时间上的解耦
- 为对象之间的解耦。它的应用非常广泛
缺点: - 创建订阅者本身要消耗一定的时间和内存
- 难以追踪定位问题
vue 中的EventBus 也是发布订阅模式
命令模式
命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些
特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收
者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
- 不关注执行者,不关注执行过程;
- 只要结果,支持撤销请求、延后处理、日志记录等
优点:
发布者与接收者实现解耦;
可扩展命令,对请求可进行排队或日志记录。(支持撤销,队列,宏命令等功能)。
缺点:
额外增加命令对象,非直接调用,存在一定开销。
组合模式
组合模式:又叫 “部分整体” 模式,将对象组合成树形结构,以表示 “部分-整体” 的层次结构。通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
- 表示 “部分-整体” 的层次结构,生成 "树叶型" 结构;
- 一致操作性,树叶对象对外接口保存一致(操作与数据结构一致);
- 自上而下的的请求流向,从树对象传递给叶对象;
- 调用顶层对象,会自行遍历其下的叶对象执行。
优缺点
- 优点:
忽略组合对象和单个对象的差别,对外一致接口使用;
解耦调用者与复杂元素之间的联系,处理方式变得简单。
- 缺点
树叶对象接口一致,无法区分,只有在运行时方可辨别;
包裹对象创建太多,额外增加内存负担。
模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常
在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺
序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
例子——Coffee or Tea
- 先泡一杯咖啡
首先,我们先来泡一杯咖啡,如果没有什么太个性化的需求,泡咖啡的步骤通常如下:
(1) 把水煮沸
(2) 用沸水冲泡咖啡
(3) 把咖啡倒进杯子
(4) 加糖和牛奶
通过下面这段代码,我们就能得到一杯香浓的咖啡
var Coffee = function(){};
Coffee.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Coffee.prototype.brewCoffeeGriends = function(){
console.log( '用沸水冲泡咖啡' );
};
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒进杯子' );
};
Coffee.prototype.addSugarAndMilk = function(){
console.log( '加糖和牛奶' );
};
Coffee.prototype.init = function(){
this.boilWater();
this.brewCoffeeGriends();
this.pourInCup();
this.addSugarAndMilk();
};
var coffee = new Coffee();
coffee.init();
- 泡一壶茶
接下来,开始准备我们的茶,泡茶的步骤跟泡咖啡的步骤相差并不大:
(1) 把水煮沸
(2) 用沸水浸泡茶叶
(3) 把茶水倒进杯子
(4) 加柠檬
同样用一段代码来实现泡茶的步骤
var Tea = function(){};
Tea.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Tea.prototype.steepTeaBag = function(){
console.log( '用沸水浸泡茶叶' );
};
Tea.prototype.pourInCup = function(){
console.log( '把茶水倒进杯子' );
};
Tea.prototype.addLemon = function(){
console.log( '加柠檬' );
};
Tea.prototype.init = function(){
this.boilWater();
this.steepTeaBag();
this.pourInCup();
this.addLemon();
};
var tea = new Tea();
tea.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();
接下来要重写抽象父类中的一些方法,只有“把水煮沸”这个行为可以直接使用父类 Beverage
中的 boilWater 方法,其他方法都需要在 Coffee 子类中重写,代码如下:
Coffee.prototype.brew = function(){
console.log( '用沸水冲泡咖啡' );
};
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒进杯子' );
};
Coffee.prototype.addCondiments = function(){
console.log( '加糖和牛奶' );
};
var Coffee = new Coffee();
Coffee.init();
至此我们的 Coffee 类已经完成了,当调用 coffee 对象的 init 方法时,由于 coffee 对象和
Coffee 构造器的原型 prototype 上都没有对应的 init 方法,所以该请求会顺着原型链,被委托给
Coffee 的“父类”Beverage 原型上的 init 方法。
而 Beverage.prototype.init 方法中已经规定好了泡饮料的顺序,所以我们能成功地泡出一杯
咖啡,代码如下:
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
接下来照葫芦画瓢,来创建我们的 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();
那么在上面的例子中,到底谁才是所谓的模板方法呢?答
案是Beverage.prototype.init。
Beverage.prototype.init 被称为模板方法的原因是,该方法中封装了子类的算法框架,它作
为一个算法的模板,指导子类以何种顺序去执行哪些方法。
模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语
言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把
这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这
部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改
动抽象父类以及其他子类,这也是符合开放封闭原则的
享元模式
享元模式(Flyweight Pattern)是一种软件设计模式。它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于当大量物件只是重复因而导致无法令人接受的使用大量内存。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。
现在假设一个一个情景, 有男女服装个100套, 需要租模特来试穿衣服, 传统做法就是男女各找100个模特试穿每一见衣服
// 雇佣模特
let HireModel = function(sex,clothes){
this.sex = sex;
this.clothes = clothes;
};
HireModel.prototype.wearClothes = function(){
console.log(this.sex + '试穿' + this.clothes);
};
/*******试穿**********/
for(let i=0;i<100;i++){
let model = new HireModel('male','第'+i+'款男衣服');
model.wearClothes();
}
for(let i=0;i<100;i++){
let model = new HireModel('female','第'+i+'款女衣服');
model.wearClothes();
}
采用享元模式则只需要男女模特各一名, 试穿所有衣服
//雇佣模特
var HireModel = function(sex){
//内部状态是性别
this.sex = sex;
};
HireModel.prototype.wearClothes = function(clothes){
console.log(this.sex+"穿了"+clothes);
};
//工厂模式,负责造出男女两个模特
var ModelFactory = (function(){
var cacheObj = {};
return {
create:function(sex){
//根据sex分组
if(cacheObj[sex]){
return cacheObj[sex];
} else {
cacheObj[sex] = new HireModel(sex);
return cacheObj[sex];
}
}
};
})();
//模特管理
var ModelManager = (function(){
//容器存储:1.共享对象 2.外部状态
var vessel = {};
return {
add:function(sex,clothes,id){
//造出共享元素:模特
var model = ModelFactory.create(sex);
//以id为键存储所有状态
vessel[id] = {
model:model,
clothes:clothes
};
},
wear:function(){
for(var key in vessel){
//调用雇佣模特类中的穿衣服方法。
vessel[key]['model'].wearClothes(vessel[key]['clothes']);
}
}
};
})();
/*******通过运行时间测试性能**********/
for(var i=0;i<100;i++){
ModelManager.add('male','第'+i+'款男衣服',i);
ModelManager.add('female','第'+i+'款女衣服',i);
}
ModelManager.wear();
享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在
大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题
参考文章
网友评论