介绍常用的Javascript设计模式。
常用设计模式分类
常用23 种设计模式可以分为三大类:
- 创建型模式(Creational Patterns)
- 结构型模式(Structural Patterns)
- 行为型模式(Behavioral Patterns)
创建型模式(5种)
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
- 工厂模式(Factory Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
结构型模式(7种)
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
- 适配器模式(Adapter Pattern)
- 装饰器模式(Decorator Pattern)
- 桥接模式(Bridge Pattern)
- 代理模式(Proxy Pattern)
- 外观模式(Facade Pattern)
- 组合模式(Composite Pattern)
- 享元模式(Flyweight Pattern)
行为型模式(11种)
这些设计模式特别关注对象之间的通信。
- 策略模式(Strategy Pattern)
- 模版模式(Template Pattern)
- 观察者模式(Observer Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 状态模式(State Pattern)
- 职责链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 备忘录模式(Memento Pattern)
- 解释器模式(Interpreter Pattern)
- 访问者模式(Visitor Pattern)
23种设计模式分类图
单例模式(Singleton Pattern)
单例模式的定义是: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
实现
简单实现
/**
* 单例模式
*/
class Singleton {
constructor(name) {
this.name = name;
this.instance = null;
}
getName(){
console.log(this.name);
}
static getInstance(name){
if (!this.instance){
this.instance = new Singleton(name);
}
return this.instance;
};
}
const a = Singleton.getInstance('sven1');
const b = Singleton.getInstance('sven2');
console.log(a === b); // true
透明的单例模式
但是这个类需要实例化后调用方法才能创建实例,我们希望直接实例化时就能创建。
/**
* 使用闭包实现单例模式
*/
const Singleton = (function(){
let instance;
class SingletonOrigin {
constructor(name) {
if (instance) {
return instance;
}
this.name = name;
instance = this;
return this;
}
getName(){
console.log(this.name);
}
}
return SingletonOrigin;
})();
const a = new Singleton('sven1');
const b = new Singleton('sven2');
console.log(a === b); // true
复制代码
为了把 instance 封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回 真正的 Singleton 构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
使用代理实现单例模式
/**
* 使用代理实现单例模式
*/
const Singleton = (function (){
class SingletonOrigin {
constructor(name) {
this.name = name;
}
getName(){
console.log(this.name);
}
}
let instance;
return function(name) {
if (!instance){
instance = new SingletonOrigin(name);
}
return instance;
}
})();
const a = new Singleton('sven1');
const b = new Singleton('sven2');
console.log(a === b); // true
惰性单例模式
前面几种实现方式是基于面向对象思路的实现,现在使用js特殊的方式实现单例模式,名为惰性单例模式
。
惰性单例模式可以推迟创建对象的时机,并不在一开始就创建,所以叫惰性
。
/**
* 惰性单例模式
*/
const getInstance = (function () {
function createInstance(name) {
return {
name: name,
getName() {
console.log(this.name);
}
}
}
function getSingle (fn){
let result;
return function() {
if (!result) {
result = fn.apply(this, arguments)
}
return result;
}
};
return getSingle(createInstance);
})();
const a = getInstance('sven1');
const b = getInstance('sven2');
console.log(a === b); // true
发布订阅模式(又叫观察者模式Observer Pattern)
实现发布订阅模式
介绍
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,一般用事件模型来替代传统的发布—订阅模式。
应用
- dom的
click
、focus
等事件 - 在Vue中的任意组件件通信事件总线
EventBus
- 含有订阅功能的系统中,如新闻app中订阅了报纸新闻;
源码
简单版
/**
* 发布订阅模式
*/
class PublishSubscribePattern {
constructor() {
// 消息映射
this.msgMap = {};
}
// 发布
publish(name, param) {
const msg = this.msgMap[name];
if (msg) {
msg.subscribes.forEach(subscribe => {
subscribe.callback(param);
});
} else {
console.log('无人订阅此消息:', name, param);
}
}
// 订阅
subscribe(name, callback) {
const msg = this.msgMap[name];
if (msg) {
msg.subscribes.push({callback});
} else {
this.msgMap[name] = {
name,
subscribes: [{callback}]
}
}
}
}
使用
const event = new PublishSubscribePattern(); event.publish('news', 'this is news 1'); event.subscribe('news', (param) => { console.log('get news:', param); }); event.publish('news', 'this is news 2');
带发布者版本
订阅者希望只订阅某一个发布者发布的消息
源码
/**
- 发布订阅模式
*/ class PublishSubscribePattern { constructor() { // 消息映射 this.msgMap = {}; } // 发布 publish({name, param, publisher}) { const msg = this.msgMap[name]; if (msg) { if (!publisher) { throw new Error('未注册发布人:' + name); } else if (publisher === 'all') { msg.subscribes.forEach(e => e.callback(param)); } else { let beAccept = false; msg.subscribes.forEach(e => { if (e.publisher === publisher) { beAccept = true; e.callback(param); } }); if (!beAccept) { console.log('无人订阅你的消息:', name, param); } } } else { console.log('无人订阅此消息:', name, param); } } // 订阅 subscribe({name, publisher}, callback) { const msg = this.msgMap[name]; if (msg) { msg.subscribes.push({ publisher, callback }); } else { this.msgMap[name] = { name, subscribes: [{ publisher, callback }] } } } }
使用
const event = new PublishSubscribePattern(); event.publish({name: 'news', param: 'this is news 1', publisher: 'weather'});
event.subscribe({name: 'news', publisher: 'weather'}, (param) => { console.log(
get news from weather:
, param); });event.publish({name: 'news', param: 'this is news 2', publisher: 'weather'}); event.publish({name: 'news', param: 'this is news of newspaper', publisher: 'newspaper'});
/* 无人订阅此消息: news this is news 1 get news from weather: this is news 2 无人订阅你的消息: news this is news of newspaper */ 复制代码
复杂版
复杂版主要实现以下功能:
取消订阅功能
订阅者希望只订阅某一个发布者发布的消息
订阅者希望获取到在订阅之前发布的历史消息
订阅者希望查看自己订阅了哪些消息
发布者希望查看自己发布过哪些消息
代码
/**
- 发布订阅模式
*/ class Event { constructor() { // 发布者映射 this.publisherMap = {}; // 订阅者映射 this.subscriberMap = {}; } // 注册 registration(username, type = 'all') { if (type === 'publisher') { this.publisherMap[username] = { publisher: username, msgMap: {} }; return this.publisherMap[username]; } else if (type === 'subscriber') { this.subscriberMap[username] = { subscriber: username, msgMap: {} }; return this.subscriberMap[username]; } else if (type === 'all') { this.publisherMap[username] = { publisher: username, msgMap: {} }; this.subscriberMap[username] = { subscriber: username, msgMap: {} }; } } // 发布 publish({name, param, publisher}) { const publisherObj = this.publisherMap[publisher]; if (!publisherObj) { throw new Error('未注册发布人:' + name); } else { const historyRecord = { name, param, publisher, time: Date.now() }; const msg = publisherObj.msgMap[name]; if (msg) { let beAccept = false; msg.subscribes.forEach(e => { if (e.publisher === publisher) { beAccept = true; e.callback(param, {name, param, publisher}); console.log(e.subscriber, '收到了', e.publisher, '发布的消息', name, param); } }); if (!beAccept) { console.log('无人订阅你的消息:', name, param); } msg.history.push(historyRecord); } else { publisherObj.msgMap[name] = { name, publisher: publisher, subscribes: [], history: [historyRecord] }; console.log('发布者', publisher, '注册消息:', name, param); } } } // 订阅 subscribe({name, publisher, subscriber, receiveHistoryMsg}, callback) { const publisherObj = this.publisherMap[publisher]; if (subscriber) { const subscriberObj = this.subscriberMap[subscriber]; if (subscriberObj) { subscriberObj.msgMap[name] = { name, publisher, subscriber: subscriber, callback, time: Date.now() }; } } if (publisherObj) { const msg = publisherObj.msgMap[name]; if (msg) { msg.subscribes.push({ publisher, subscriber, callback }); console.log(subscriber || '游客', '订阅了', publisher, '的消息:', name); if (receiveHistoryMsg === true) { msg.history.forEach(e => callback(e.param, e)); } } else { console.log('发布者', publisher, '未注册过此消息:', name); } } else { console.log('发布者未注册:', publisher); } } // 取消订阅 unsubscribe({name, publisher, subscriber}) { const publisherObj = this.publisherMap[publisher]; if (subscriber) { const subscriberObj = this.subscriberMap[subscriber]; if (subscriberObj) { delete subscriberObj.msgMap[name]; } } if (publisherObj) { const msg = publisherObj.msgMap[name]; if (msg) { msg.subscribes = msg.subscribes.filter(e => !(e.publisher === publisher && msg.name === name)); } else { console.log('发布者', publisher, '未注册过此消息:', name); } } else { console.log('发布者未注册:', publisher); } } // 获取发布历史消息 getPublishHistory(publisher, name) { return this.publisherMap[publisher].msgMap[name].history; } getSubscribeMsg(subscriber) { return this.subscriberMap[subscriber].msgMap; } }
使用
// 直接使用Event实现发布订阅功能 const event = new Event();
const publisher = 'A';
const subscriber = 'B';
event.registration(publisher); event.registration(subscriber);
const name = 'news';
const param = '一条消息a';
event.publish({name, publisher, param});
event.subscribe({name, publisher, subscriber, receiveHistoryMsg: true}, (param, e) => { console.log(
---- 接收消息from:
, param, e); });event.publish({name, publisher, param: '一条消息b'});
console.log('订阅的消息', event.getSubscribeMsg(subscriber));
event.unsubscribe({name, publisher, subscriber});
event.publish({name, publisher, param: '一条消息c'});
console.log('发布历史', event.getPublishHistory(publisher, name));
/* 发布者 A 注册消息: news 一条消息a B 订阅了 A 的消息: news ---- 接收消息from: 一条消息a { name: 'news', param: '一条消息a', publisher: 'A', time: 1603011782573 } ---- 接收消息from: 一条消息b { name: 'news', param: '一条消息b', publisher: 'A' } B 收到了 A 发布的消息 news 一条消息b 订阅的消息 { news: { name: 'news', publisher: 'A', subscriber: 'B', callback: [Function], time: 1603011782575 } } 无人订阅你的消息: news 一条消息c 发布历史 [ { name: 'news', param: '一条消息a', publisher: 'A', time: 1603011782573 }, { name: 'news', param: '一条消息b', publisher: 'A', time: 1603011782577 }, { name: 'news', param: '一条消息c', publisher: 'A', time: 1603011782578 } ] */
使用适配器类
代码
/**
- 代理类,屏蔽重复设置发布者、订阅者
*/ class Factory { constructor(username, type) { this.username = username; this.type = type; this._event = new Event(); this._event.registration(username, type || 'all'); } // 发布 publish(param) { return this._event.publish(Object.assign({}, param, {publisher: this.username})) } // 订阅 subscribe(param, callback) { return this._event.subscribe(Object.assign({}, param, {subscriber: this.username}), callback); } // 取消订阅 unsubscribe(param) { return this._event.unsubscribe(Object.assign({}, param, {subscriber: this.username})); } // 获取历史发布消息 getPublishHistory(name) { return this._event.getPublishHistory(this.username, name); } // 获取订阅的消息列表 getSubscribeMsg() { return this._event.getSubscribeMsg(this.username); }
}
使用
// 使用适配器封装 const publisherA = 'A'; const subscriberB = 'B'; const publisher = new Factory(publisherA, 'publisher'); const subscriber = new Factory(subscriberB, 'subscriber');
const name = '新闻';
publisher.publish({name, param: 'this is news 1'});
subscriber.subscribe({name, publisher: publisherA, receiveHistoryMsg: true}, (param) => { console.log(
---- get news from ${publisherA}:
, param); }); console.log('订阅的消息', subscriber.getSubscribeMsg());publisher.publish({name, param: 'this is news 2'});
publisher.publish({name, param: 'this is news of newspaper'});
console.log('发布历史', publisher.getPublishHistory(name));
/* 发布者 A 注册消息: 新闻 this is news 1 发布者未注册: A 订阅的消息 { '新闻': { name: '新闻', publisher: 'A', subscriber: 'B', callback: [Function], time: 1603012329816 } } 无人订阅你的消息: 新闻 this is news 2 无人订阅你的消息: 新闻 this is news of newspaper 发布历史 [ { name: '新闻', param: 'this is news 1', publisher: 'A', time: 1603012329813 }, { name: '新闻', param: 'this is news 2', publisher: 'A', time: 1603012329819 }, { name: '新闻', param: 'this is news of newspaper', publisher: 'A', time: 1603012329819 } ] */
中介者模式(Mediator Pattern)
中介者模式
的作用就是解除对象与对象之间的紧耦合关系。
介绍
增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
中介者模式是迎合最少知识原则
(迪米特法则)的一种实现。是指一个对象应 该尽可能少地了解另外的对象(类似不和陌生人说话)。
举例说明
用一个小游戏说明中介者模式的用处。
**游戏规则:**两组选手进行对战,其中一个玩家死亡的时候游戏便结束, 同时通知它的对手胜利。
普通实现
var players = [];
//接着我们再来编写Hero这个函数;代码如下:
var players = []; // 定义一个数组 保存所有的玩家
function Hero(name,teamColor) {
this.friends = []; //保存队友列表
this.enemies = []; // 保存敌人列表
this.state = 'live'; // 玩家状态
this.name = name; // 角色名字
this.teamColor = teamColor; // 队伍的颜色
}
Hero.prototype.win = function(){
console.log("win:" + this.name);
};
Hero.prototype.lose = function(){
console.log("lose:" + this.name);
};
Hero.prototype.die = function(){
// 所有队友死亡情况 默认都是活着的
var all_dead = true;
this.state = 'dead'; // 设置玩家状态为死亡
for(var i = 0,ilen = this.friends.length; i < ilen; i+=1) {
// 遍历,如果还有一个队友没有死亡的话,则游戏还未结束
if(this.friends[i].state !== 'dead') {
all_dead = false;
break;
}
}
if(all_dead) {
this.lose(); // 队友全部死亡,游戏结束
// 循环 通知所有的玩家 游戏失败
for(var j = 0,jlen = this.friends.length; j < jlen; j+=1) {
this.friends[j].lose();
}
// 通知所有敌人游戏胜利
for(var j = 0,jlen = this.enemies.length; j < jlen; j+=1) {
this.enemies[j].win();
}
}
}
// 定义一个工厂类来创建玩家
var heroFactory = function(name,teamColor) {
var newPlayer = new Hero(name,teamColor);
for(var i = 0,ilen = players.length; i < ilen; i+=1) {
// 如果是同一队的玩家
if(players[i].teamColor === newPlayer.teamColor) {
// 相互添加队友列表
players[i].friends.push(newPlayer);
newPlayer.friends.push(players[i]);
}else {
// 相互添加到敌人列表
players[i].enemies.push(newPlayer);
newPlayer.enemies.push(players[i]);
}
}
players.push(newPlayer);
return newPlayer;
};
// 红队
var p1 = heroFactory("aa",'red'),
p2 = heroFactory("bb",'red'),
p3 = heroFactory("cc",'red'),
p4 = heroFactory("dd",'red');
// 蓝队
var p5 = heroFactory("ee",'blue'),
p6 = heroFactory("ff",'blue'),
p7 = heroFactory("gg",'blue'),
p8 = heroFactory("hh",'blue');
// 让红队玩家全部死亡
p1.die();
p2.die();
p3.die();
p4.die();
// lose:dd lose:aa lose:bb lose:cc
// win:ee win:ff win:gg win:hh
复制代码
中介者模式实现
玩家与玩家之间的耦合代码解除,把所有的逻辑操作放在中介者对象里面进去处理,某个玩家的任何操作不需要去遍历去通知其他玩家,而只是需要给中介者发送一个消息即可,中介者接受到该消息后进行处理,处理完消息之后会把处理结果反馈给其他的玩家对象。
var players = []; // 定义一个数组 保存所有的玩家
function Hero(name,teamColor) {
this.state = 'live'; // 玩家状态
this.name = name; // 角色名字
this.teamColor = teamColor; // 队伍的颜色
}
Hero.prototype.win = function(){
// 赢了
console.log("win:" + this.name);
};
Hero.prototype.lose = function(){
// 输了
console.log("lose:" + this.name);
};
// 死亡
Hero.prototype.die = function(){
this.state = 'dead';
// 给中介者发送消息,玩家死亡
playerDirector.ReceiveMessage('playerDead',this);
}
// 移除玩家
Hero.prototype.remove = function(){
// 给中介者发送一个消息,移除一个玩家
playerDirector.ReceiveMessage('removePlayer',this);
};
// 玩家换队
Hero.prototype.changeTeam = function(color) {
// 给中介者发送一个消息,玩家换队
playerDirector.ReceiveMessage('changeTeam',this,color);
};
// 定义一个工厂类来创建玩家
var heroFactory = function(name,teamColor) {
// 创建一个新的玩家对象
var newHero = new Hero(name,teamColor);
// 给中介者发送消息,新增玩家
playerDirector.ReceiveMessage('addPlayer',newHero);
return newHero;
};
var playerDirector = (function(){
var players = {}, // 保存所有的玩家
operations = {}; // 中介者可以执行的操作
// 新增一个玩家操作
operations.addPlayer = function(player) {
// 获取玩家队友的颜色
var teamColor = player.teamColor;
// 如果该颜色的玩家还没有队伍的话,则新成立一个队伍
players[teamColor] = players[teamColor] || [];
// 添加玩家进队伍
players[teamColor].push(player);
};
// 移除一个玩家
operations.removePlayer = function(player){
// 获取队伍的颜色
var teamColor = player.teamColor,
// 获取该队伍的所有成员
teamPlayers = players[teamColor] || [];
// 遍历
for(var 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) {
var teamColor = player.teamColor,
// 玩家所在的队伍
teamPlayers = players[teamColor];
var all_dead = true;
//遍历
for(var i = 0,player; player = teamPlayers[i++]; ) {
if(player.state !== 'dead') {
all_dead = false;
break;
}
}
// 如果all_dead 为true的话 说明全部死亡
if(all_dead) {
for(var i = 0, player; player = teamPlayers[i++]; ) {
// 本队所有玩家lose
player.lose();
}
for(var color in players) {
if(color !== teamColor) {
// 说明这是另外一组队伍
// 获取该队伍的玩家
var teamPlayers = players[color];
for(var i = 0,player; player = teamPlayers[i++]; ) {
player.win(); // 遍历通知其他玩家win了
}
}
}
}
};
var ReceiveMessage = function(){
// arguments的第一个参数为消息名称 获取第一个参数
var message = Array.prototype.shift.call(arguments);
operations[message].apply(this,arguments);
};
return {
ReceiveMessage : ReceiveMessage
};
})();
// 红队
var p1 = heroFactory("aa",'red'),
p2 = heroFactory("bb",'red'),
p3 = heroFactory("cc",'red'),
p4 = heroFactory("dd",'red');
// 蓝队
var p5 = heroFactory("ee",'blue'),
p6 = heroFactory("ff",'blue'),
p7 = heroFactory("gg",'blue'),
p8 = heroFactory("hh",'blue');
// 让红队玩家全部死亡
p1.die();
p2.die();
p3.die();
p4.die();
// lose:aa lose:bb lose:cc lose:dd
// win:ee win:ff win:gg win:hh
复制代码
策略模式(Strategy Pattern)
要实现某一个功能有多种方案可以选择,定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换,这就是策略模式。
举例
- 表单校验:
执行校验规则
和校验规则配置
分开; - 前端动画类:
将渲染动画
、动画配置
以及动画控制
分开
策略模式演示:表单校验
// 校验方法&规则配置
var strategies = {
isNonEmpty: function( value, errorMsg ){ // 不为空
if ( value === '' ){
return errorMsg ;
}
},
minLength: function( value, length, errorMsg ){ // 限制最小长度
if ( value.length < length ){
return errorMsg;
}
},
isMobile: function( value, errorMsg ){ // 手机号码格式
if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
return errorMsg;
}
}
};
// 校验执行器
var Validator = function(){
this.cache = []; // 保存校验规则
};
Validator.prototype.add = function( dom, rule, errorMsg ){
var ary = rule.split( ':' ); // 把strategy 和参数分开
this.cache.push(function(){ // 把校验的步骤用空函数包装起来,并且放入cache
var strategy = ary.shift(); // 用户挑选的strategy
ary.unshift( dom.value ); // 把input 的value 添加进参数列表
ary.push( errorMsg ); // 把errorMsg 添加进参数列表
return strategies[ strategy ].apply( dom, ary );
});
};
Validator.prototype.start = function(){
for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if ( msg ){ // 如果有确切的返回值,说明校验没有通过
return msg;
}
}
};
// 控制器
var validataFunc = function(){
var validator = new Validator(); // 创建一个validator 对象
/***************添加一些校验规则****************/
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );
validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6 位' );
validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' );
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
}
// 程序入口
var registerForm = document.getElementById( 'registerForm' );
registerForm.onsubmit = function(){
var errorMsg = validataFunc(); // 如果errorMsg 有确切的返回值,说明未通过校验
if ( errorMsg ){
alert ( errorMsg );
return false; // 阻止表单提交
}
};
复制代码
优点
策略模式是一种常用且有效的设计模式,总结一下策略模式的一些优点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
缺点
策略模式也有一些缺点,但这些缺点并不严重:
- 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的 逻辑堆砌在 Context 中要好。
- 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点, 这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选 择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少 知识原则的。
总结
不仅是算法,业务规则指向的目标一致,并且可以被替换使用,就也可以用策略模式来封装它们。
“在函数作为一等对象的语言中,策略模式是隐形的。 strategy 就是值为函数的变量。”
相对传统面向对象语言的方式实现策略模式,使用 JavaScript 语言的策略模式,策略类往往被函数所代替,这时策略模式就 成为一种“隐形”的模式。
代理模式(Proxy Pattern)
为其他对象提供一种代理以控制对这个对象的访问。
常用的代理模式变种有以下几种:
- 保护代理
- 虚拟代理
- 缓存代理
1. 保护代理
保护代理用于控制不同权限的对象对目标对象的访问
举例
class Car { drive() { return "driving"; }; }
class CarProxy { constructor(driver) { this.driver = driver; } drive() { // 保护代理,仅18岁才能开车 return (this.driver.age < 18) ? "too young to drive" : new Car().drive(); }; } 复制代码
2. 虚拟代理
虚拟代理可应用于:
图片懒加载
、惰性加载
、合并http请求
等
举例:图片懒加载
图片懒加载 复制代码
3. 缓存代理
缓存代理可应用于:
缓存ajax异步请求数据
、计算乘积
等
举例:缓存ajax请求数据
const getData = (function() { const cache = {}; return function(url) { if (cache[url]) { return Promise.resolve(cache[url]); } return $.ajax.get(url).then((res) => { cache[url] = res; return res; }).catch(err => console.error(err)) } })();
getData('/getData'); // 发起http请求 getData('/getData'); // 返回缓存数据 复制代码
总结
代理模式包括许多小分类,在 JavaScript 开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。
迭代器模式(Iterator Pattern)
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象 5 的内部表示。
迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
例如 Javascript中的forEach
、map
、some
等;
迭代器可分为一下两种:
- 内部迭代器
- 外部迭代器
内部迭代器
内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。
例如:
function each(ary, callback) { for (let i = 0; i < ary.length; i++){ callback.call(ary[i], i, ary[i]); } } each([1, 2, 3], (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
}
};
复制代码
总结: 外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。内部迭代器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定。
迭代类数组对象和字面量对象
迭代器模式不仅可以迭代数组,还可以迭代一些类数组的对象。
无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代。
例如:arguments
、 {'0': 'a', '1': 'b'}
// 迭代器支持类数组和对象的遍历
function each(obj, callback) {
var value, i = 0, length = obj.length, isArray = isArraylike( obj );
if ( isArray ) {
for ( ; i < length; i++ ) {
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
} else {
// 迭代object 对象
for ( i in obj ) {
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
}
return obj;
};
复制代码
倒序迭代器
从尾到头
的遍历数组。
中止迭代器
在遍历过程中,如果满足某种条件可以终止迭代。
function each(ary, callback){
for (let i = 0; i < ary.length; i++){
// callback 的执行结果返回false,提前终止迭代
if (callback(i, ary[i]) === false ){
break;
}
}
};
复制代码
迭代器模式的应用举例
- nodejs的
express
框架的中间件思想,处理请求用到的next()
方法就是迭代器模式。 - ECMAScript 6 的
Iterator
(遍历器)和Generator
异步编程中使用了next()
适配器模式(Adapter Pattern)
适配器模式的作用是解决两个接口/方法间的接口不兼容的问题。
作为两个不兼容的接口之间的桥梁,就是新增一个包装类,对新的接口进行包装以适应旧代码的调用,避免修改接口和调用代码。
示例
// 1\. 方法适配 *******************************
const A = {
show() {
console.log('visible');
}
}
const B = {
display() {
console.log('visible');
}
}
// 不使用适配器
A.show();
B.display();
// 使用适配器
const C = {
show() {
B.display();
}
}
A.show();
C.show();
// 2\. 接口适配 *******************************
const data1 = {name: 'alan'};
const data2 = {username: 'tom'};
function sayName(param) {
console.log(param.name);
}
function adapter(param) {
return {name: param.username}
}
sayName(data1);
sayName(adapter(data2));
复制代码
相似模式之间的差异
有一些模式跟适配器模式的 结构非常相似,比如装饰者模式、代理模式和外观模式。
这几种模式都属于“包 装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。
适配器模式
主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实 现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够 使它们协同作用。装饰者模式
和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象 增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理 模式是为了控制对对象的访问,通常也只包装一次。外观模式
的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外 观模式最显著的特点是定义了一个新的接口。
总结
适配器模式是作为一个中间的桥梁,使原本有差异的接口变成需要的标准,能够满足现有接口的需要,而不需要去修改现有的源码。
命令模式(Command Pattern)
命令模式将调用者和执行者之间分开,通过命令来映射各种操作,从而达到松耦合的目的。
命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。
应用场景
有时候需要向某些对象发送请求,但是并不知道请求的接收 者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
命令模式的调用者只需要下达命令,不需要知道命令被执行的具体细节。从而使调用者专注于自己的主流程。
特点
- 松耦合:将请求调用者和请求接收者松耦合
- 生命周期
- 支持撤销
- 支持排队
举例
命令模式实现向左、向右、及撤销操作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>命令模式</title>
<style>
#app{width: 100%;max-width: 400px;margin: 0 auto;}
.container{width: 100%;background-color: #ddd;margin-top: 40px;}
.box{width: 25px;height: 15px;background-color: #ff5b5c;}
</style>
</head>
<body>
<div id="app">
<h1>命令模式</h1>
<button id="left">向左</button>
<button id="right">向右</button>
<button id="turnback">撤销</button>
<div style="margin-top: 20px"><strong> history: </strong><span id="history" ></span></div>
<div class="container">
<div id="box" class="box"></div>
</div>
</div>
<script>
const step = 25;
const right = document.getElementById('right');
const left = document.getElementById('left');
const turnback = document.getElementById('turnback');
const historyLog = document.getElementById('history');
class Box{
constructor({pos = 0, id}) {
this.pos = pos;
this.$box = document.getElementById(id);
this.update();
}
moveTo(pos) {
this.pos = pos;
this.update();
}
update() {
this.$box.style.marginLeft = this.pos + 'px';
}
}
class Command {
constructor(box) {
this.pos = box.pos || 0;
this.box = box;
this.history = [this.pos];
historyLog.innerText = this.history;
}
run(dis) {
this.pos = this.pos + dis;
this.box.moveTo(this.pos);
this.history.push(this.pos);
historyLog.innerText = this.history;
console.log('run:', this.pos, this.history);
}
cancel() {
if (this.history.length === 0) {alert('回到起点');return}
const record = this.history.pop();
this.pos = this.history[this.history.length - 1];
this.box.moveTo(this.pos);
historyLog.innerText = this.history;
console.log('cancel:', record, this.history);
}
}
const box = new Box({id: 'box', pos: 2 * step});
const command = new Command(box);
right.addEventListener('click', () => command.run(step));
left.addEventListener('click', () => command.run(-step));
turnback.addEventListener('click', () => command.cancel());
</script>
</body>
</html>
复制代码
组合模式(Composite Pattern)
组合模式(Composite Pattern),又叫
部分整体模式
,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
当我们想执行一个宏大的任务时,这个任务可以被细分为多层结构,组合模式
可以让我们只发布一次执行命令,就能完成整个复杂的任务,屏蔽层级关系
和差异化问题
。
注意
- 组合模式不是父子关系,它们能够合作的关键是拥有相同的接口;
- 对叶对象操作的一致性,要对每个目标对象都实行同样的操作;
- 可用中介者模式处理双向映射关系,例如一个子节点同时在不同的父节点中存在(组织架构);
- 可用职责链模式提高组合模式性能,通过设置链条避免每次都遍历整个树;
应用场景
组合模式应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。
- 命令分发: 只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合
开放-封闭原则
; - 统一处理: 统一对待树中的所有对象,忽略组合对象和叶对象的区别;
比如: 文件目录显示,多级目录呈现等树形结构数据的操作。
举例:文件系统操作
// 文件夹
var Folder = function( name ){
this.name = name;
this.parent = null; //增加this.parent 属性
this.files = [];
};
Folder.prototype.add = function( file ){
file.parent = this; //设置父对象
this.files.push( file );
};
Folder.prototype.scan = function(){
console.log( '开始扫描文件夹: ' + this.name );
for ( var i = 0, file, files = this.files; file = files[ i++ ]; ){
file.scan();
}
};
Folder.prototype.remove = function(){
if ( !this.parent ){ //根节点或者树外的游离节点
return;
}
for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
var file = files[ l ];
if ( file === this ){
files.splice( l, 1 );
}
}
};
// 文件
var File = function( name ){
this.name = name;
this.parent = null;
};
File.prototype.add = function(){
throw new Error( '不能添加在文件下面' );
};
File.prototype.scan = function(){
console.log( '开始扫描文件: ' + this.name );
};
File.prototype.remove = function(){
if ( !this.parent ){ //根节点或者树外的游离节点
return;
}
for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
var file = files[ l ];
if ( file === this ){
files.splice( l, 1 );
}
}
};
var folder = new Folder( '学习资料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new Folder ( '深入浅出Node.js' );
folder1.add( new File( 'JavaScript 高级程序设计' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除文件夹
folder.scan();
复制代码
总结
组合模式可以让我们使用树形方式创 建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都 可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。
模板方法模式(Template Method)
模板方法模式是一种通过封装变化提高系统扩展性的设计模式。
在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把 这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这 部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改 动抽象父类以及其他子类,这也是符合开放-封闭原则
的。
应用场景
假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。 但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。
组成
模板方法模式由两部分结构组成:
- 抽象父类: 通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行方式(比如执行顺序、条件执行等)。
- 具体的实现子类: 子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
示例
模板方法模式通常通过继承来实现,在 JavaScript 开发中用到继承的场景其实并不是很多,很多时候我们都喜欢用 mix-in 的方式给对象扩展属性。
抽象类
定义执行方法的方式(比如执行顺序、条件执行等),它的子类可以按需要重写被执行的方法,核心是抽象类。
class Tax {
calc(value) {
if (value >= 1000)
value = this.overThousand(value);
return this.complementaryFee(value);
}
complementaryFee(value) {
return value + 10;
}
}
class Tax1 extends Tax {
constructor() {
super();
}
overThousand(value) {
return value * 1.1;
}
}
复制代码
享元模式(Flyweight Pattern)
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。
享元模式属于结构型
模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
适应场景
解决的问题: 在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
使用注意: 使用了享元模式之后,我们需要分别多维护一个 factory 对象和一个 manager 对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。
因此我们可以在以下场景中使用享元模式:
- 一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 对象的大多数状态都可以变为外部状态。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
内部状态与外部状态
享元模式要求将对象的属性划分为内部状态
与外部状态
(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,那么如何划分内部状态和外部状态呢?
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态 可以从对象身上剥离出来,并储存在外部。
剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整 的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系 统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间
的优化模式。
使用享元模式的关键是如何区别内部状态
和外部状态
。
举例
代码关键点:用 HashMap 对象池存储这些对象。
// 享元模式,对象池缓存对象 class colorFactory { constructor(name) { this.colors = {}; } create(name) { let color = this.colors[name]; if (color) return color; this.colors[name] = new Color(name); return this.colors[name]; } }; 复制代码
优缺点
优点: 大大减少对象的创建,降低系统的内存,使效率提高。
缺点: 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
模职责链模式 (Chain of Responsibility Pattern)
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推,直到有一个对象处理它为止。
这种类型的设计模式属于行为型模式
。
适用场景
职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
- 有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可动态指定一组对象处理请求。
应用示例
JS 中的事件冒泡 express、koa中间件洋葱模型
实现简易koa中间件【职责链模式】
class Middleware { constructor() { this.middlewares = []; } use(fn) { if(typeof fn !== 'function') { throw new Error('Middleware must be function, but get ' + typeof fn); } this.middlewares.push(fn); return this; } compose() { const middlewares = this.middlewares; return dispatch(0); function dispatch(index) { const middleware = middlewares[index]; if (!middleware) {return;} try{ const ctx = {}; const result = middleware(ctx, dispatch.bind(null, index + 1)); return Promise.resolve(result); } catch(err) { return Promise.reject(err); } } } }
const middleware = new Middleware(); middleware.use(async (ctx, next) => { console.log(1); await next(); console.log(2); }); middleware.use(async (ctx, next) => { console.log(3); await next(); console.log(4); });
middleware.compose();// 1 3 4 2 复制代码
优缺点
优点:
- 降低耦合度。它将请求的发送者和接收者解耦。
- 简化了对象。使得对象不需要知道链的结构。
- 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
- 增加新的请求处理类很方便。
缺点:
- 不能保证请求一定被接收。
- 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
- 可能不容易观察运行时的特征,有碍于除错。
总结
职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职 责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。
Javascript 设计模式 - 状态模式
当控制对象状态的条件表达式过于复杂时的情况,把状态的判断逻辑转移到表示不同的状态的一系列类或者方法当中,可以把复杂的逻辑判断简单化。
状态模式
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
这种类型的设计模式属于行为型
模式。
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context
对象。
介绍
意图: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决: 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用: 代码中包含大量与对象状态有关的条件语句。
如何解决: 将各种具体的状态类抽象出来。
使用场景:
- 行为随状态改变而改变的场景。
- 条件、分支语句的代替者。
注意事项: 在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
优缺点
优点:
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
应用
- 文件下载(开始、暂停、完成、失败等)
- 游戏(走动、攻击、防御、跌倒、跳跃)
- 红绿灯(红、绿、黄切换)
状态模式和策略模式的关系
状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,但在意图上有很大不同,因此它们是两种迥然不同的模式。
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请 求委托给这些类来执行。
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系, 所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态 和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情 发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
总结
状态模式可用来优化条件分支语句
、含有大量状态且行为随状态改变而改变
的场景。一开始不太好理解,但使用得当可以让代码的结构变得非常清晰和可扩展。
参考
我平时一直有整理面试题的习惯,有随时跳出舒适圈的准备,不知不觉整理了229页了,在这里分享给大家,有需要的点击这里免费领取题目+解析PDF
篇幅有限,仅展示部分内容
如果你需要这份完整版的面试题+解析,【点击我】就可以了。
希望大家明年的金三银四面试顺利,拿下自己心仪的offer!