前端js用得上的设计模式
以下内容来源于《JavaScript设计模式与开发实践》
经典的书籍,值得反复去学习和品味,每一次看都能得到新的认识,因为我们一直在进步,观察问题的视角也一直在上升。
单例模式
单例模式的核心是确保只有一个实例,并提供全局访问。
var obj;
if (!obj) {
obj = xxx;
}
惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实 际开发中非常有用,有用的程度可能超出了我们的想象
我们把如何管理单例的逻辑从业务的代码中抽离出来,这些逻辑被封装在 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);
总结:单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
奖金计算
// before
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
// after
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
表单校验
<html>
<body>
<form action="http:// xxx.com/register" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName" />
请输入密码:<input type="text" name="password" />
请输入手机号码:<input type="text" name="phoneNumber" />
<button>提交</button>
</form>
<script>
/***********************策略对象**************************/
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;
}
}
};
/***********************Validator 类**************************/
var Validator = function () {
this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0, rule; rule = rules[i++];) {
(function (rule) {
var strategyAry = rule.strategy.split(':');
var errorMsg = rule.errorMsg;
self.cache.push(function () {
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rule)
}
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var errorMsg = validatorFunc();
if (errorMsg) {
return errorMsg;
}
}
};
/***********************客户调用代码**************************/
var registerForm = document.getElementById('registerForm');
var validataFunc = function () {
var validator = new Validator();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:10',
errorMsg: '用户名长度不能小于10 位'
}]);
validator.add(registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6 位'
}]);
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '手机号码格式不正确'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
};
</script>
</body>
</html>
优缺点
优
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy 中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context 中要好。
缺: 要使用策略模式,必须了解所有的strategy,必须了解各个strategy 之间的不同点, 这样才能选择一个合适的strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选 择飞机、火车、自行车等方案的细节。此时strategy 要向客户暴露它的所有实现,这是违反最少 知识原则的。
代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象
图片预加载
var myImage = (function () {
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
},
};
})();
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage.setSrc(this.src);
};
return {
setSrc: function (src) {
myImage.setSrc("file:// /C:/Users/svenzeng/Desktop/loading.gif");
img.src = src;
},
};
})();
proxyImage.setSrc("http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg");
单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
给img 节点设置src 和图片预加载这两个功能,被隔离在两个对象里,它们可以自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。
发布—订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。
全局的发布订阅对象
实际使用场景如: 消息模块的未读通知 一般我们PC官网都有个消息通知模块,有这么个需求,当从消息列表中 进入详情,layout层header的消息通知icon的右上角的小红点需要重新调用接口获取未读消息数。这时候就可以在layout层的header模块注册监听(listen)在回调里执行函数调用,在消息详情接口触发后trigger
const globalEvent = (function () {
const listener = {};
const listen = (key, fn) => {
if (!listener[key]) {
listener[key] = [];
}
listener[key].push(fn);
}
const remove = (key, fn) => {
const fns = listener[key];
if (!fns) {
// 如果 key 对应的消息没有被人订阅,则直接返回
return;
}
if (!fn) {
fns = []
return;
}
for (let i = fns.length - 1; i >= 0; i--) {
// 反向遍历订阅的回调函数列表
const _fn = fns[ i ];
if (_fn === fn) {
fns.splice(i, 1);
}
}
}
const trigger = (key, val) => {
const fns = listener[key];
if (!fns || fns.length === 0) {
return;
}
for (let i = 0, fn; (fn = fns[i++]); ) {
fn(val);
}
}
return {
listen,
remove,
trigger,
}
}());
globalEvent.listen('squareMeter88', (fn1 = function (price) {
console.log('价格= ' + price);
})
);
globalEvent.listen('squareMeter88', (fn2 = function (price) {
console.log('价格= ' + price);
})
);
globalEvent.remove('squareMeter88', fn1); // 删除订阅
globalEvent.trigger('squareMeter88', 2000000); // 输出:2000000
先订阅后发布,并且加了命名空间的处理
const eventGlobal = (function () {
const event = (function () {
const namespaceMap = {};
const defaultName = "__default__";
const _listen = (key, cache, fn) => {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
const _remove = (key, cache, fn) => {
const fns = cache[key];
// 没有已监听函数
if (!fns || !fns.length) {
return;
}
if (!fn) {
cache[key] = [];
return;
}
for (let i = fns.length - 1; i >= 0; i--) {
const _fn = fns[i];
if (_fn === fn) {
fns.splice(i, 1);
}
}
};
const _trigger = (key, cache, val) => {
const fns = cache[key];
each(fns, function () {
this.call(null, val);
});
};
const each = (fns, fn) => {
fns.map((_fn) => fn.call(_fn));
};
const _create = (name) => {
const nameSpace = name || defaultName;
let offlineStack = [];
const cache = {};
const ret = {
listen: (key, fn) => {
_listen(key, cache, fn);
if (!offLineStack) {
return;
}
each(offLineStack, function () {
this();
});
offlineStack = false;
},
remove: (key, fn) => _remove(key, cache, fn),
trigger: (key, val) => {
const fn = () => _trigger(key, cache, val);
if (!offLineStack) {
return fn();
}
offLineStack.push(fn);
},
};
return namespaceMap[nameSpace] || (namespaceMap[nameSpace] = ret);
};
return {
create: _create,
listen: (key, fn) => {
const event = _create();
event.listen(key, fn);
},
remove: (key, fn) => {
const event = _create();
event.remove(key, fn);
},
trigger: (key, fn) => {
const event = _create();
event.trigger(key, fn);
},
};
})();
return event;
})();
/************** 先发布后订阅 ********************/
eventGlobal.trigger("click", 1);
eventGlobal.trigger("click", 12);
eventGlobal.listen("click", function (a) {
console.log(a); // 输出:1
});
/************** 使用命名空间 ********************/
eventGlobal.create("namespace1").trigger("click", 8);
eventGlobal.create("namespace1").listen(
"click",
(fn1 = function (a) {
console.log(a); // 输出:1
})
);
eventGlobal.create("namespace1").remove("click", fn1);
eventGlobal.create("namespace1").trigger("click", 10);
发布—订阅模式在实际开发中非常有用。发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC 还是MVVM, 都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。 当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个bug 不是件轻松的事情。
职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
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("手机库存不足");
}
};
// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
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;
};
// 现在我们把3 个订单函数分别包装成职责链的节点:
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); // 输出:500 元定金预购,得到100 优惠券
chainOrder500.passRequest(2, true, 500); // 输出:200 元定金预购,得到50 优惠券
chainOrder500.passRequest(3, true, 500); // 输出:普通购买,无优惠券
chainOrder500.passRequest(1, false, 0); // 输出:手机库存不足
/* 通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员
又想出了支持300 元定金购买,那我们就在该链中增加一个节点即可:
*/
var order300 = function () {
// 具体实现略
};
chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);
小结:在JavaScript 开发中,职责链模式是最容易被忽视的模式之一。实际上只要运用得当,职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。
中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系
和发布—订阅模式的区别:
发布—订阅模式模式:只能从从一的一方循环的通知,属于单向。 中介者模式:可以从任一方循环通知,属于双向。
泡泡堂游戏
function Player(name, teamColor) {
this.name = name; // 角色名字
this.teamColor = teamColor; // 队伍颜色
this.state = "alive"; // 玩家生存状态
}
Player.prototype.win = function () {
console.log(this.name + " won ");
};
Player.prototype.lose = function () {
console.log(this.name + " lost");
};
/*******************玩家死亡*****************/
Player.prototype.die = function () {
this.state = "dead";
playerDirector.reciveMessage("playerDead", this); // 给中介者发送消息,玩家死亡
};
/*******************移除玩家*****************/
Player.prototype.remove = function () {
playerDirector.reciveMessage("removePlayer", this); // 给中介者发送消息,移除一个玩家
};
/*******************玩家换队*****************/
Player.prototype.changeTeam = function (color) {
playerDirector.reciveMessage("changeTeam", this, color); // 给中介者发送消息,玩家换队
};
var playerFactory = function (name, teamColor) {
var newPlayer = new Player(name, teamColor); // 创造一个新的玩家对象
playerDirector.reciveMessage("addPlayer", newPlayer); // 给中介者发送消息,新增玩家
return newPlayer;
};
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;
}
}
if (all_dead === true) {
// 全部死亡
for (var i = 0, player; (player = teamPlayers[i++]); ) {
player.lose(); // 本队所有玩家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 reciveMessage = function () {
var message = Array.prototype.shift.call(arguments); // arguments 的第一个参数为消息名称
operations[message].apply(this, arguments);
};
return {
reciveMessage: reciveMessage,
};
})();
// 红队:
var player1 = playerFactory("皮蛋", "red"),
player2 = playerFactory("小乖", "red"),
player3 = playerFactory("宝宝", "red"),
player4 = playerFactory("小强", "red");
// 蓝队:
var player5 = playerFactory("黑妞", "blue"),
player6 = playerFactory("葱头", "blue"),
player7 = playerFactory("胖墩", "blue"),
player8 = playerFactory("海盗", "blue");
player1.die();
player2.die();
player3.die();
player4.die();
小结:中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。
中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。
装饰者模式
装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象 动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓,遇到一堆食尸鬼时就点开AOE(范围攻击)技能。
数据统计上报
页面中有一个登录button,点击这个button 会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button
<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};
var showLogin = function () {
console.log("打开登录浮层");
};
var log = function () {
console.log("上报标签为: " + this.getAttribute("tag"));
};
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById("button").onclick = showLogin;
</script>
</html>
与代理模式的区别
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。
总结
学以致用,扩充思维的广度和深度。 理想很美好,现实很残酷,这句话只有在亲身经历了项目重构才能深得体会,项目的历史债务,终究有一代人要还的。
转载自:https://juejin.cn/post/7147302421511798821