设计模式
设计模式一般在软件开发的过程中有所应用,但在前端开发过程中也有时候会用到。个人理解设计模式是为了解决特定的问题而提出的一种解决方案,像是一个模板,用了可以使代码更加简洁和更加容易维护,可读性和结构性也会更强。
下面记录一下本次学习的4种比较常用的设计模式:单例模式、观察者模式、发布订阅模式、策略模式。
单例模式
如名称一般,单例模式下需要保证的有两点:
- 确保如何情况下都绝对只有一个实例
- 想在程序上表现出“只存在一个实例”
根据这两点可以得到如下代码:
function Person() {
this.name = 'Jack'
}
let instance = null
function singleTon() {
if (!instance) instance = new Person()、
return instance
}
const p1 = singleTon()
const p2 = singleTon()
console.log(p1 === p2)//true
可以看到现在不论调用多少次singleTon,都只会创建后自返回同一个实例。但这种做法有两个问题,明明是Person的实例,却需要通过singleTon来调用,和构造函数看起来没什么关系了;在创建实例的时候从表面看也丢失了new关键字。根据这两个问题对其进行改造。
const Person = (function () { //直接以构造函数名称命名
function Person() { //把构造函数放到内部
this.name = 'Jack'
this.age = 18
}
Person.prototype.sayHi = function () { console.log('Hello, my name is', this.name) }
// 利用闭包的形式来使instance一直存在
let instance = null
return function singleTon() {
if (!instance) instance = new Person()
return instance
}
})() //立即执行函数
const p1 = new Person()
const p2 = new Person()
console.log(p1 === p2) //true
可以看到这次把两个问题都解决了。主要就是利用闭包来延长instance的生命周期,这里解释一下为什么可以使用new。new的本质其实是新建一个对象通过修改this来对新对象进行赋值,如果构造函数有返回一个对象了,那么就不会返回新建的对象。上述代码中会return instance,所以用不用new关键字都可以。
做个小练习,做一个自定义弹出层。
首先定义一个弹出层的类,并且点击拥有回调函数
class Tip {
constructor() {
this.ele = document.createElement('div')
this.ele.className = 'tip'
//绑定事件
this.bindEvent()
//设置回调函数
this.callback = function () {}
document.body.appendChild(this.ele)
}
//设置提示内容
//实际上应该一个节点一个节点创建,不能这么写,容易被注入恶意代码
setContent(txt) {
this.ele.innerHTML = `
<div class="top">
<div>标题</div>
<button id='cancle'>X</button>
</div>
<div class="content">
<p>${txt}</p>
</div>
<div class="btns">
<button id='cancle'>取消</button>
<button id='ok'>确定</button>
</div>
`
this.ele.style.display = 'block'
}
bindEvent() {
this.ele.addEventListener('click', e => {
e = e || window.event
const target = e.target || e.srcElement
if (target.id === 'cancle') {
this.ele.style.display = 'none'
} else if (target.id === 'ok') {
this.callback() //ok的时候执行回调函数
this.ele.style.display = 'none'
}
})
}
}
const tip = new Tip()
tip.setContent('hahaha', () => { console.log('回调函数1')})
tip.setContent('hello world', () => { console.log('回调函数2')})
接下来就要用到单例模式了,按照上面那样把类包在一个函数里面:
const Tip = (function () {
class Tip {...}
let instance = null
return function singleTon(txt, cb) {
if (!instance) instance = new Tip()
instance.setContent(txt) //设置提示内容
instance.callback = cb //设置回调函数
return instance
}
})()
const tip1 = new Tip("你好", () => {
console.log("回调函数1")
})
const tip2 = new Tip("世界", () => {
console.log("回调函数2")
})
console.log(tip1 === tip2) //true
不论我们new多少次,只有一个实例对象,这很好地节约了内存,也省去了每次创建和删除节点了工作。
在这里我们第一个参数只传了字符串,实际上可以传一个配置项,可以修改样式等等,这样子自由度就高了很多。
单例模式适用于模板不变,内容变的情景,例如本例。
观察者模式
观察者模式有两个对象,一个是观察者,一个是被观察者,当被观察者的状态发生改变的时候,需要通知观察者,观察者会做出相对的反应。
//观察者
class Observer {
constructor(name, fn = () => { }) {
this.name = name
this.fn = fn
}
}
//被观察者
class Subject {
constructor(name, state) {
this.name = name
this.state = state
this.observers = []
}
//设置状态
setState(val) {
this.state = val
//通知观察者
this.notify()
}
//添加观察者
addObserver(obs) {
//不存在才能添加
if (this.observers.indexOf(obs) == -1) {
this.observers.push(obs)
}
}
//删除观察者
deleteObserver(obs) {
//使用过滤,得到不包含obs的数组
this.observers = this.observers.filter(item => item !== obs)
}
//通知观察者
notify() {
this.observers.forEach(item => {
item.fn(this.name, this.state)
})
}
}
const obs1 = new Observer("观察者1", (sub, state) => { console.log('观察者1发现' + sub + '的状态修改为' + state) })
const obs2 = new Observer("观察者2", (sub, state) => { console.log('观察者2发现' + sub + '的状态修改为' + state) })
const sub = new Subject("被观察者", "学习")
sub.addObserver(obs1)
sub.addObserver(obs2)
sub.setState("玩游戏")
sub.addObserver(obs2)
console.log(sub.observers)
sub.deleteObserver(obs1)
console.log(sub.observers)
sub.setState("打球")
观察者模式适用于根据对象状态进行相应处理的场景。
发布订阅模式
对观察者模式和发布订阅模式,有人认为是一样的,也有人认为不一样。不一样的原因的发布订阅模式在结构上和观察者模式就有区别。观察者模式是在被观察者状态发生改变的时候一一通知观察者,他们都知道对方的存在。而发布订阅模式,在发布者和订阅者中间有一个中介,负责对事件进行调度,发布者和订阅者不知道对方是否存在。
class PubSub {
constructor() {
//订阅列表
this.subscribers = {}
}
//订阅类型和处理函数
subscribe(type, fn = () => { }) {
if (!this.subscribers[type]) {
this.subscribers[type] = []
}
this.subscribers[type].push(fn)
}
//取消订阅
unsubscribe(type, fn) {
// 1、取消整个类型的订阅
if (!fn) {
delete this.subscribers[type]
}
// 2、取消具体的订阅
if (!this.subscribers[type]) return
this.subscribers[type] = this.subscribers[type].filter(item => item !== fn)
}
//发布
publish(type, ...args) {
if (!this.subscribers[type]) return
this.subscribers[type].forEach(fn => {
fn(...args)
})
}
}
const pb = new PubSub()
pb.subscribe("小说", (val) => console.log(val))
pb.publish("小说", "您订阅的小说更新了")
pb.subscribe("动漫", (val) => console.log(val))
pb.publish("动漫", "您订阅的动漫托更了")
注意这里使用箭头函数(匿名函数)当作回调函数是不能取消订阅的了,因为找不到他对应的地址了,所以最好把函数单独写出来。
可以看到和观察者模式还是有点区别的,发布者和订阅者的耦合度更低了,代码也更加的简洁。
策略模式
策略(Strategy)模式可以整体地替换算法的实现部分,能让我们轻松地以不同的算法去解决同一个问题。这里提出一个应用场景,就是我们常见的购物车,在结算的时候我们可能有多种折扣方案可以选择,我们可以任意选择其中一种方案,有时我们还能看到该优惠券不可用等- -。
那么现在就有一个问题,优惠的方案是会不断变化的,对应的算法也会变化,我们总不能每次都去修改源代码的算法部分吧,我们需要更加便捷的方法,能够快速添加和删除一种方案,并且选择某种方案后可以计算出优惠后的价格。
const calPrice = (function () {
//折扣类型以及计算方法
const sale = {
'100-10': function (price) { return price -= 10 },
'200-25': function (price) { return price -= 25 },
'90%': function (price) { return price *= 0.9 }
}
//返回使用折扣后的价格
function calPrice(type, price) {
if (!sale[type]) return '该折扣不可用'
return sale[type](price)
}
//添加一种折扣
calPrice.add = function (type, fn) {
if (sale[type]) return '该折扣已存在'
sale[type] = fn
}
//删除一种折扣
calPrice.del = function (type) {
delete sale[type]
}
//获取所有折扣
calPrice.getDis = function () {
return Object.keys(sale)
}
//闭包
return calPrice
})()
calPrice.add('618', (price) => { return price *= 0.8 })
console.log(calPrice('618', 500))
console.log(calPrice.getDis())
calPrice.del('100-10')
console.log(calPrice.getDis())
在这种结构下,我们可以主动添加或者删除折扣,也可以列出所有的折扣种类,然后根据用户选择的折扣来返回计算的结果。设想一下,如果是写成一堆if和else,那么每次修改折扣的时候就要修改源代码,不容易维护和更加任意出错。
所以设计模式是非常重要的,选择好了以后轻松很多。这次先学习这4种设计模式,后面有再学习的时候会继续补充,如果有问题欢迎提出~