0. 回顾过去
说实话,这标题有点儿 uc小编 的味道了。虽然真正的大佬已经对设计模式烂熟于胸,只希望我的学习记录能帮助到部分同学就足够了。经过我下面的介绍,你可以在极短的时间,了解并知道如何使用他们。
经过一两个月的分享断断续续的,我一共分享了 11 种 Javascript 设计模式,其中:
- 工厂模式,单例模式,适配器模式,装饰者模式,建造者模式 过去看看
- 构造函数模式,外观模式,代理模式 用英雄联盟的方式讲解JavaScript设计模式!
- 策略模式,桥接模式,状态模式 过去看看
每一种设计模式都是前辈们总结多年的经验,实属精华。尽管我不能完全出神入化的运用它,但在一个程序但设计上,一定会或多或少的来借鉴他们的思想。自从学习了设计模式,在写代码的时候,终于不会气喘吁吁,一口气写五个组件,不费劲。
「那下面,接着介绍四种,令人拍案叫绝的!设计模式。」
1. 原型模式
简介
原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
「说实话,每次一看设计模式的简介就头大,明明每个字都认识,结合起来就懵了。和我一样情况的小伙伴,建议直接看代码,有的时候...看看注释,看看设计模式的名字,也许就豁然开朗!」
实际操作
原型模式的使用可以让我们得到,原始对象附带的一些属性,如下:
var lol = {
server: '比尔吉沃特',
startGame: function () {
console.log('link start!')
}
};
// 通过原型模式,新建同一个服务器的新用户
var User = Object.create(lol, {
'name': {
value: '黄梵高'
}
});
上面可以说就是一个完整的原型模式。把原有对象内包含的属性,通过Object.create
函数,成功拷贝。并且在创建新对象时,添加进入新增的name
字段,十分人性化。
如果说你觉得,哦,我这要兼容 ie8 的,用不来Object.create
这种高级浏览器才支持的函数。那完全可以的,下面再来介绍不使用这函数的原型模式实现:
var lol = {
server: '比尔吉沃特',
startGame: function () {
console.log('link start!')
},
init: function (name) { // 需要增加一个接口,用于修改内部属性
this.name = name
},
};
function User(name) {
function F() { };
F.prototype = lol;
var f = new F();
f.init(name);
return f;
}
var user = User('黄梵高');
user.startGame();
// user.server
这种原型模式的实现方式,和上面的Object.create
方式略有不同,因为创建了一个名叫F
的构造函数,并且提前暴漏了接口init
才得以修改内部属性,和直接创建对象相比自然是冗余了不少。
不过相信兼容 ie8 的需求也还是存在的,很多时候还是不得不使用它啊!
那恭喜你学会了原型模式,因为十分简单,平时开发也会不经意间用到,但要注意浅拷贝和深拷贝的问题哦。先来道开胃菜,这波啊这波是一道肉蛋葱鸡。
2. 观察者模式
介绍
观察者模式定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
实践环节
其实观察者模式,真的太熟悉了。就是当你开源项目终于发布了,你希望很多人都知道,但不能你一个一个去告诉吧,那也太卑微了。所以最好的方式是你有一群粉丝,他们翘首以盼着等着你的开源项目,终于你在b站,开了一个发布会。很多粉丝争先恐后地去听,听完之后回家根据这个新框架开始练手。这就构成了一个观察者模式。
观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
先来一个错误的❌例子:
var timer = setInterval(() => {
if (window.good) {
clearInterval(timer)
console.log('good了')
}
}, 500)
setTimeout(() => {
window.good = 1
}, 2000)
s: 实现成功!教练我学会了!这边疯狂修改全局变量,另一边疯狂
event loop
就好了,500
毫秒延迟太高了,我再低一点,调个50
吧,再来一个 for 循环,遍历创建事件就可以了!t: 且不说性能问题,那如果页面很多,你在某一个页面抛出的变量一定能接的到吗?或者会不会有变量冲突,多人维护的时候怎么能保证全局变量的统一处理?
s: 那教练我不会了。
t: 下面教练教你一招,通过存放回调函数的方式队列执行:
var pubsub = {};
(function (q) {
var topics = {}, // 回调函数存放的数组,把所有的
subUid = -1;
// 发布方法
q.publish = function (topic, args) {
if (!topics[topic]) {
return false;
}
setTimeout(function () {
var subscribers = topics[topic], // 名字为topic变量的事件队列
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, args); // 循环执行传进来的函数
}
}, 0); // 使用setTimeout保证先执行完同步代码逻辑
return true;
};
//订阅方法
q.subscribe = function (topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
//退订方法
q.unsubscribe = function (token) {
for (var m in topics) { // 先找到要退订的事件队列
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) { // 传进来的token,是在触发订阅函数时生成的token
if (topics[m][i].token === token) { // 找到专属token,进行队列删除操作
topics[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
} (pubsub));
//将订阅赋值给一个变量,以便退订
var sub = pubsub.subscribe('lol', function (topics, data) {
console.log(topics + ": " + data);
});
//发布通知
pubsub.publish('lol', 'hello world!');
pubsub.publish('lol', ['test', 'a', 'b', 'c']);
pubsub.publish('lol', [{ 'color': 'blue' }, { 'text': 'hello'}]);
setTimeout(function () {
pubsub.unsubscribe(sub);
}, 0);
pubsub.publish('lol', 'hello world!'); // 不会再执行订阅时传入的函数了
以上就实现了一个,以事件回调为基础的观察者模式模型。拥有订阅,发布,退订操作。「可以满足大部分对于一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变」的情况。
通过事件队列依次执行,如果需要增强函数功能,只需要扩展函数即可。
另外一个观察者显而易见的例子:如果你平时使用 vue
React
等框架的话,里面的 redux
vuex
就涉及到观察者模式。
小结
但是,观察者虽好,可不要贪杯嗷。为什么这么说呢?
- 一个观察者模式的实现,难免需要很长的逻辑,难免影响一些内存
- 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。
- 观察者模式可以使观察者知道所观察的对象发生了变化,但观察者模式没有使观察者知道所观察的对象是怎么发生变化的。
- 观察者模式让一个本可以不触发的事件,一直停留在内存中,即使事件不会执行。
- 弱化了对象之间的联系,会导致项目的难以跟踪维护和理解。
只要不滥用,观察者模式还是能够让逻辑分离,实现代码解耦,提高可维护程度。总比用setInterval
来循环监听全局变量要好得多吧。
3. 迭代器模式
简介
迭代器模式(Iterator):提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。
其实就是在不暴露对象的情况,可以拿到所有对象内的 元素。
其实ES6设计了Iterator
的概念
下面是阮大官网介绍的栗子
实际操作
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
简单看一下代码,很清晰,十分易于理解,迭代器模式要的就是不需要关心数据结构,但只要我们调用it.next()
方法,就可以知道对象所有的内容,按照顺序返回给我们!
而我们并不会暴露it
对象里面所有的数据,十分人性化。
但是需要注意的是,在JavaScript
的世界里,并非所有对象都有迭代器,类数组,数组,字符串是拥有迭代器的,但一个普通的对象必须部署了 Iterator
接口后才能使用。
那我们再举个栗子🌰:
var obj = {
name: 123,
age: 18,
info: [{
friend: 'abc',
title: 'ba'
}]
}
//
这样的一个对象,如果想能拿到所有对象,但不把对象暴露出来该怎么办呢?
这种情况对于前端开发可以说司空见惯了,后端给的数据一定是多层嵌套的,只要是 JavaScript 有的数据类型,肯定都有机会出现,那如果希望遍历取值怎么办?
相信Object.keys()
是很多人的选择!
for (var key of Object.keys(obj)) {
console.log(key + ': ' + obj[key]);
}
//name: 123
//age: 18
//info: [object Object]
小结
上面就实现了迭代器模式,很多人都认为迭代器模式比较简单,甚至很多语言都会内置迭代器,方便使用,更有甚者认为这不属于一种设计模式。
其实设计模式也就是前人总结的设计经验,建筑学中有着更多的设计模式,软件工程学中有着相比之下较少,但精华的模式,正因为它十分优秀,才会在多种编程语言下大放异彩,而不应因为它被内置而不再提及。
4. 中介者模式
中介者模式,一看名字就知道,应该是有一个东西作为两端沟通的中介。比如,我们平时租房买房,难免要和中介打交道,很多时候就被中介把买卖双方都坑了,这种时候就是个十分差劲的中介者模式实践😠!
简介
中介者模式(Mediator),用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介者模式的本质就是,有一个集权的管理控制函数,能够做到单向的接受信息并进行分发消息和动作。
与观察者模式的区别
这两种模式真的很像,于是网络上很容易的找到一段不明所以的话。
❝观察者模式,没有封装约束的单个对象,相反,观察者
❞Observer
和具体类Subject
是一起配合来维护约束的,沟通是通过多个观察者和多个具体类来交互的:每个具体类通常包含多个观察者,而有时候具体类里的一个观察者也是另一个观察者的具体类。而中介者模式所做的不是简单的分发,却是扮演着维护这些约束的职责。
这句话可以说,十分晦涩难懂...
用点清楚的白话来描述二者区别:
「观察者模式,肯定会有一个观察者的列表,其中可能会有增加,删除,插入,置空等等接口函数。调用观察者函数的,可以通过这些接口,进行一些操作,也就是和原有的观察者函数,共同来维护观察者列表!」
「中介者模式特点就是:不需要调用中介者函数的,来进行一些对中介者列表对处理。因为这份列表,是中介者函数提供的,并不需要共同维护,只需要中介者函数自己来维护。然后和观察者模式同样的,拥有分发和监听对功能!」
差别也就是,具体的监听列表,能不能用调用它对函数进行维护。
实践环节
function LOL (username) {
this.username = username
this.task = {}
}
LOL.prototype.on = function (type, callback) {
this.task[type].push(callback)
}
LOL.prototype.emit = function (type) {
this.task[type].forEach(item => {
item && item()
})
}
var mine = new LOL('黄梵高')
mine.on('start', function(){
console.log('start')
})
mine.emit('start')
上面的手写代码,可以说观察者模式和中介者模式,都可以这么实现。具体区别就是观察者模式的话,实现还应该会多出unemit, empty
等等函数,便于操作观察者列表。
中介者模式内部应该还会有一些其他处理,比如:
function LOL (username) {
this.username = username
this.task = {}
}
LOL.prototype.on = function (type, callback) {
this.task[type].push(callback)
}
LOL.prototype.emit = function (type) {
if (type === 'stop') { // 如果事件为停止,则把start列表全部清空
this.task['start'] = []
}
this.task[type].forEach(item => {
item && item()
})
}
如上注释处,中介者模式会把维护列表的工作,与自己融为一体,省着你在外面操作。
中介者模式也同样可以用,买卖租房的中介来理解。无论中介是好是坑,其实作为找中介的我们,也没办法去改变它给我们提供的房屋列表。
小结
中介者模式并不困难,某种程度上和观察者实现差不多。当然二者也有区别,刚才也已经叙述。请根据业务需求具体使用。
总结
设计模式可以帮助我们设计函数结构,易于维护,开发也可以避免失误。但过度设计也会造成资源浪费,开发周期增加等缺点,所以一定要适度结合使用。在频繁改动的项目,即使你设计的十分优雅,也有可能直接被产品把功能砍掉...无论怎样抽象解耦,也一定要适度而行。比如我目前维护的项目,频繁使用观察者模式并不适合,会造成很多的资源浪费,某些情况下,甚至调整 dom 资源加载顺序也可以解决一些问题(开发时可能会有很多种不同方案,请用性价比最高的方案!)。
参考文献
本文使用 mdnice 排版