前言
设计模式是一种思想,就像是前人记录下来的经验,就好比是厨师日积月累的经验撰写出来的菜谱
这是定义:
优雅是指不粗暴,简洁是因为规范化了,我们需要在日常开发中潜意识里去用这样一种思想,这样团队协作,后期的维护、迭代等等之类的会避免不少麻烦
这里只介绍在 JavaScript 中常见的几种设计模式,其他的设计模式详见 参考文章
单例模式
应用场景
一个 button
的点击事件是触发弹窗,那无论用户是否重复点击,弹窗只会创建一个
定义
具体实现
实现的方法为先判断实例存在与否
,如果存在则 直接返回,如果不存在就 创建了再返回,这就确保了一个类只有一个实例对象。
其中利用了:ES6 的 let 不允许重复声明的特性
方法一:if...else 来判断是否已经存在
let Singleton = function(name){
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function(){
console.log(this.name);
}
Singleton.getInstance = function(name){
if(this.instace){
return this.instance;
}
return this.instance = new Singleton(name);
}
let winner = Singleton.getInstance("winner");
console.log(winner.getName()); // winner
let sunner = Singleton.getInstance("sunner");
console.log(sunner.getName()); // winner
缺点:创建对象 的操作和 判断实例 的操作耦合
在一起,并不符合“单一职责原则
”
方法二:利用自执行函数,将创建对象和判断实例分开
let ProxyCreateSingleton = (function(){
let instance = null;
return function(name){
if(instance){
return instance
}
return instance = new Singlton(name);
}
})();
let Singlton = function(name){
this.name = name;
}
Singlton.prototype.getName = function(){
console.log(this.name);
}
let winner = new ProxyCreateSingleton("winner");
console.log(winner.getName()); // winner
let sunner = new ProxyCreateSingleton("sunner");
console.log(sunner.getName()); // winner
上面的代码中,ProxyCreateSingleton()只负责 判断实例,Singlton只负责 创建对象和赋值
策略模式
应用场景
举个非常简单的例子,一个检验表单的程序:用户输入姓名、性别、年龄、身份证号、手机号等等信息后提交表单,我们首先要检查哪一项没有填写,然后再各自检查输入内容是否合法。
定义
具体实现
1. 使用策略模式前:一大堆的if...return...
var checkAuth = function(data){
if(data.name === null){
console.log("姓名项不能为空") // 当然里面还可以继续写其他对结果的处理代码,这里省略了
}
if(data.gender === null){
console.log("性别项不能为空")
}
if(data.age === null){
console.log("年龄项不能为空")
}
if(data.id === null){
console.log("您未输入身份证号")
}
if(data.phoneNumber === null){
console.log("您未输入手机号")
}
}
缺点:
- 时间上不够优化
- checkAuth 函数会爆炸 ?
- 策略项无法复用
- 违反开闭原则
2. 使用策略模式后:obj + function
/*策略类*/
var checkObj = {
"name": function(value) {
if(!value) return "姓名项不能为空";
},
"gender" : function(value) {
if(!value) return "性别项不能为空";
},
"age" : function(value) {
if(!value) return "年龄项不能为空";
},
"id" : function(value) {
if(!value) return "您未输入身份证号";
},
"phoneNumber" : function(value) {
if(!value) return "您未输入手机号";
}
};
/*环境类*/
var calculateBouns = function(data) {
var arr = Object.entires(data)
for(let i in arr){
var res = checkObj[ arr[i][0] ]( arr[i][1] )
console.log(res)
}
};
// 还能只单独调用其中一个方法
var res = checkObj["age"]("");
console.log(res) // "年龄项不能为空"
这里只是简单举例,在 data 对象里直接拿取其属性去 checkObj 对象里面找由其命名的方法,然后传入对应属性的值,即可快速抵达。然后后期对各个方法修改起来也方便
这是两种方式的对比:
发布 - 订阅模式
应用场景
需求 : 申请成功后,需要触发对应的订单、消息、审核模块对应逻辑
还有其他类似场景:怎么模仿 报社发报的流程 做 公众号推文发布和通知 的功能、小程序抽奖结果通知等等
定义
具体实现
1. 使用发布 - 订阅模式前:
一个函数包裹三个函数
function applySuccess() {
MessageCenter.fetch(); // 通知消息中心获取最新内容
Order.update(); // 更新订单信息
Checker.alert(); // 通知相关方审核
}
缺点:
- 如果上面的函数还没完成 / 有错误,则下面的函数就会被卡死(无法运行、无法调试)
- 本应该是并行处理的,结果是串行处理
2. 使用发布 - 订阅模式后:
实现思路
- 创建一个对象
- 在该对象上创建一个缓存列表(调度中心)
- on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
- emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
// 公众号对象
let eventEmitter = {};
// 缓存列表,存放 event 及 fn
eventEmitter.list = {};
// 订阅
eventEmitter.on = function (event, fn) {
let that = this;
// 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
// 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
(that.list[event] || (that.list[event] = [])).push(fn);
return that;
};
// 发布
eventEmitter.emit = function () {
let that = this;
// 第一个参数是对应的 event 值,直接用数组的 shift 方法取出
let event = [].shift.call(arguments),
fns = [...that.list[event]];
console.log(event) // 'notice'
console.log(fns) // [f user1(content), f user2(content), f user3(content)]
// 如果缓存列表里没有 fn 就返回 false
if (!fns || fns.length === 0) {
return false;
}
// 遍历 event 值对应的缓存列表,依次执行 fn
fns.forEach(fn => {
fn.apply(that, arguments);
});
return that;
};
function user1 (content) {
if(content === 'success') console.log('收到申请成功的通知,开始执行 MessageCenter.fetch 函数');
};
function user2 (content) {
if(content === 'success') console.log('收到申请成功的通知,开始执行 Order.update 函数');
};
function user3 (content) {
if(content === 'success') console.log('收到申请成功的通知,开始执行 Checker.alert 函数');
};
// 订阅者发起订阅
eventEmitter.on('notice', user1);
eventEmitter.on('notice', user2);
eventEmitter.on('notice', user3);
// 发布者发布内容
eventEmitter.emit('notice', 'success');
补充 once 、off 方法
- off 方法可以
根据 event 值
取消订阅(取消订阅) - once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
let eventEmitter = {
// 缓存列表
list: {},
// 订阅
on (event, fn) {
let _this = this;
// 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
// 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
(_this.list[event] || (_this.list[event] = [])).push(fn);
return _this;
},
// 监听一次
once (event, fn) {
// 先绑定,调用后删除
let _this = this;
function on () {
_this.off(event, on);
fn.apply(_this, arguments);
}
on.fn = fn;
_this.on(event, on);
return _this;
},
// 取消订阅
off (event, fn) {
let _this = this;
let fns = _this.list[event];
// 如果缓存列表中没有相应的 fn,返回false
if (!fns) return false;
if (!fn) {
// 如果没有传 fn 的话,就会将 event 值对应缓存列表中的 fn 都清空
fns && (fns.length = 0);
} else {
// 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可
let cb;
for (let i = 0, cbLen = fns.length; i < cbLen; i++) {
cb = fns[i];
if (cb === fn || cb.fn === fn) {
fns.splice(i, 1);
break
}
}
}
return _this;
},
// 发布
emit () {
let _this = this;
// 第一个参数是对应的 event 值,直接用数组的 shift 方法取出
let event = [].shift.call(arguments),
fns = [..._this.list[event]];
// 如果缓存列表里没有 fn 就返回 false
if (!fns || fns.length === 0) {
return false;
}
// 遍历 event 值对应的缓存列表,依次执行 fn
fns.forEach(fn => {
fn.apply(_this, arguments);
});
return _this;
}
};
function user1 (content) {
console.log('用户1订阅了:', content);
}
function user2 (content) {
console.log('用户2订阅了:', content);
}
function user3 (content) {
console.log('用户3订阅了:', content);
}
function user4 (content) {
console.log('用户4订阅了:', content);
}
// 订阅
eventEmitter.on('article1', user1);
eventEmitter.on('article1', user2);
eventEmitter.on('article1', user3);
// 取消user2方法的订阅
eventEmitter.off('article1', user2);
eventEmitter.once('article2', user4)
// 发布
eventEmitter.emit('article1', 'Javascript 发布-订阅模式');
eventEmitter.emit('article1', 'Javascript 发布-订阅模式');
eventEmitter.emit('article2', 'Javascript 观察者模式');
eventEmitter.emit('article2', 'Javascript 观察者模式');
// eventEmitter.on('article1', user3).emit('article1', 'test111');
由打印结果可看出:
- user2 虽然 on 订阅了但是
又 off 取消了订阅
,所以发布者发布的所有内容 user2全都没有收到
- 只有 user4 订阅了 article2 ,所以虽然发布者
发布了两次
article2 的内容,但是 user4只收到一次
观察者模式
应用场景
应用场景与发布-订阅模式类似
定义
可以理解为:一个班里的学生们都在听老师讲课,当老师布置任务时,会通知学生们都去执行
具体实现
// 被观察者
function Subject(){
this.observers = [];
}
Subject.prototype = {
// 添加观察者
add: function(observer) {
this.observers.push(observer);
},
// 通知观察者
notify: function(){
var observers = this.observers;
var len = observers.length;
for(var i=0; i<len; i++){
observers[i].update();
}
},
// 移除观察者
remove: function(observer) {
var observers = this.observers;
var len = observers.length;
for(var i=0; i<len; i++){
if(observers[i] === observer) {
observers.splice(i, 1);
}
}
}
}
// 观察者
function Observer(name) {
this.name = name;
}
Observer.prototype = {
// 观察者监听到变化后要处理的逻辑
update: function(){
console.log('收到通知,我是观察者:', this.name);
}
}
// 创建观察者
var observer1 = new Observer('John');
var observer2 = new Observer('Alice');
// 添加观察者
var subject = new Subject();
subject.add(observer1);
subject.add(observer2);
// 发起通知
subject.notify();
发布-订阅模式与观察者模式的区别
简单来说,发布订阅模式 比 观察者模式 多了一层 Event Channel
观察者模式:观察者(Observer)直接订阅
(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。
发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度
(Fire Event)订阅者注册到调度中心的处理代码。
差异:
- 在观察者模式中,观察者是
知道 Subject
的,Subject 一直保持对观察者进行记录。而在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理
进行通信。 - 在发布订阅模式中,组件是
松散耦合
的,正好和观察者模式相反。 - 观察者模式大多数时候是
同步
的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步
的(使用消息队列)。 - 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅模式更像交叉应用模式。
参考文章
- JavaScript设计模式——单例模式
- JavaScript 发布-订阅模式
- 前端渣渣唠嗑一下前端中的设计模式(真实场景例子)
- JavaScript设计模式
- JavaScript 中常见设计模式整理
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!