JavaScript 的一些常用设计模式
设计模式是前人解决某个特定场景下对而总结出来的一些解决方案。可能刚开始接触编程还没有什么经验的时候,会感觉设计模式没那么好理解,这个也很正常。有些简单的设计模式我们有时候用到,不过没意识到也是存在的。
学习设计模式,可以让我们在处理问题的时候提供更多更快的解决思路。
当然设计模式的应用也不是一时半会就会上手,很多情况下我们编写的业务逻辑都没用到设计模式或者本来就不需要特定的设计模式。
适配器模式
这个使我们常使用的设计模式,也算最简单的设计模式之一,好处在于可以保持原有接口的数据结构不变动。
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。
例子
适配器模式很好理解,假设我们和后端定义了一个接口数据结构为(可以理解为旧接口):
[ { "label": "选择一", "value": 0 }, { "label": "选择二", "value": 1 } ]
但是后端后面因为其他原因,需要定义返回的结构为(可以理解为新接口):
[ { "label": "选择一", "text": 0 }, { "label": "选择二", "text": 1 } ]
然后我们前端的使用到后端接口有好几处,那么我可以把新的接口字段结构适配为老接口的,就不需要各处去修改字段,只要把源头的数据适配好就可以了。
当然上面的是非常简单的场景,也是经常用到的场景。或许你会认为后端处理不更好了,的确是这样更好,但是这个不是我们讨论的范围。
单例模式
单例模式,从字面意思也很好理解,就是实例化多次都只会有一个实例。
有些场景实例化一次,可以达到缓存效果,可以减少内存占用。还有些场景就是必须只能实例化一次,否则实例化多次会覆盖之前的实例,导致出现 bug(这种场景比较少见)。
例子
实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销, 我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例, 从而节省部分 DOM 开销。下列为登入框部分代码:
const createLoginLayer = function() { const div = document.createElement('div') div.innerHTML = '登入浮框' div.style.display = 'none' document.body.appendChild(div) return div }
使单例模式和创建弹框代码解耦
const getSingle = function(fn) { const result return function() { return result || result = fn.apply(this, arguments) } } const createSingleLoginLayer = getSingle(createLoginLayer) document.getElementById('loginBtn').onclick = function() { createSingleLoginLayer() }
代理模式
代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理对象拥有本体对象的一切功能的同时,可以拥有而外的功能。而且代理对象和本体对象具有 一致的接口
,对使用者友好。
虚拟代理
下面这段代码运用代理模式来实现图片预加载,可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离,,并且在未来如果不需要预加载,只要改成请求本体代替请求代理对象就行。
const myImage = (function() { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: function(src) { imgNode.src = src } } })() const proxyImage = (function() { const img = new Image() img.onload = function() { // http 图片加载完毕后才会执行 myImage.setSrc(this.src) } return { setSrc: function(src) { myImage.setSrc('loading.jpg') // 本地 loading 图片 img.src = src } } })() proxyImage.setSrc('http://loaded.jpg')
缓存代理
在原有的功能上加上结果缓存功能,就属于缓存代理。
原先有个功能是实现字符串反转(reverseString),那么在不改变 reverseString
的现有逻辑,我们可以使用缓存代理模式实现性能的优化,当然也可以在值改变的时候去处理下其他逻辑,如 Vue computed 的用法。
function reverseString(str) { return str .split('') .reverse() .join('') } const reverseStringProxy = (function() { const cached = {} return function(str) { if (cached[str]) { return cached[str] } cached[str] = reverseString(str) return cached[str] } })()
订阅发布模式
订阅发布使前端常用的数据通信方式、异步逻辑处理等等,如 React setState 和 Redux 就是订阅发布模式的。
但是要合理的使用订阅发布模式,否则会造成数据混乱,redux 的单向数据流思想可以避免数据流混乱的问题。
例子
class Event { constructor() { // 所有 eventType 监听器回调函数(数组) this.listeners = {} } /** * 订阅事件 * @param {String} eventType 事件类型 * @param {Function} listener 订阅后发布动作触发的回调函数,参数为发布的数据 */ on(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = [] } this.listeners[eventType].push(listener) } /** * 发布事件 * @param {String} eventType 事件类型 * @param {Any} data 发布的内容 */ emit(eventType, data) { const callbacks = this.listeners[eventType] if (callbacks) { callbacks.forEach((c) => { c(data) }) } } } const event = new Event() event.on('open', (data) => { console.log(data) }) event.emit('open', { open: true })
观察者模式
观察者模式
定义了一种一对多的依赖关系,让多个 观察者
对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有 观察者
对象,使它们能够自动更新。
Vue 的数据驱动就是使用观察者模式,mbox 也是使用观察者模式。
例子
模仿 Vue 数据驱动渲染模式(只是类似,简单的模仿)。
首先使用 setter 和 getter 监听到数据的变化:
const obj = { data: { description: '' }, } Object.defineProperty(obj, 'description', { get() { return this.data.description }, set(val) { this.data.description = val }, })
然后加上目标和观察者
class Subject { constructor() { this.observers = [] } add(observer) { this.observers.push(observer) } notify(data) { this.observers.forEach((observer) => observer.update(data)) } } class Observer { constructor(callback) { this.callback = callback } update(data) { this.callback && this.callback(data) } } // 创建观察者ob1 let ob1 = new Observer((text) => { document.querySelector('#dom-one').innerHTML(text) }) // 创建观察者ob2 let ob2 = new Observer((text) => { document.querySelector('#dom-two').innerHTML(text) }) // 创建目标sub let sub = new Subject() // 目标sub添加观察者ob1 (目标和观察者建立了依赖关系) sub.add(ob1) // 目标sub添加观察者ob2 sub.add(ob2) // 目标sub触发事件(目标主动通知观察者) sub.notify('这里改变了')
组合在一起是这样的
class Subject { constructor() { this.observers = [] } add(observer) { this.observers.push(observer) } notify() { this.observers.forEach((observer) => observer.update()) } } class Observer { constructor(callback) { this.callback = callback } update() { this.callback && this.callback() } } const obj = { data: { description: '' }, } // 创建观察者ob1 const ob1 = new Observer(() => { console.log(document.querySelector('#dom-one')) document.querySelector('#dom-one').innerHTML = obj.description }) // 创建观察者ob2 const ob2 = new Observer(() => { document.querySelector('#dom-two').innerHTML = obj.description }) // 创建目标sub const sub = new Subject() // 目标sub添加观察者ob1 (目标和观察者建立了依赖关系) sub.add(ob1) // 目标sub添加观察者ob2 sub.add(ob2) Object.defineProperty(obj, 'description', { get() { return this.data.description }, set(val) { this.data.description = val // 目标sub触发事件(目标主动通知观察者) sub.notify() }, }) btn.onclick = () => { obj.description = '改变了' }原来的值
原来的值
装饰者模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。
ES6/7 的 decorator
语法提案,就是装饰者模式。
例子
class A { getContent() { return '第一行内容' } render() { document.body.innerHTML = this.getContent() } } function decoratorOne(cla) { const prevGetContent = cla.prototype.getContent cla.prototype.getContent = function() { return ` 第一行之前的内容
${prevGetContent()} ` } return cla } function decoratorTwo(cla) { const prevGetContent = cla.prototype.getContent cla.prototype.getContent = function() { return ` ${prevGetContent()}
第二行内容 ` } return cla } const B = decoratorOne(A) const C = decoratorTwo(B) new C().render()
策略模式
在策略模式(Strategy Pattern)中,一个行为或其算法可以在运行时更改。
假设我们的绩效分为 A、B、C、D 这四个等级,四个等级的奖励是不一样的,一般我们的代码是这样实现:
/** * 获取年终奖 * @param {String} performanceType 绩效类型, * @return {Object} 年终奖,包括奖金和奖品 */ function getYearEndBonus(performanceType) { const yearEndBonus = { // 奖金 bonus: '', // 奖品 prize: '', } switch (performanceType) { case 'A': { yearEndBonus = { bonus: 50000, prize: 'mac pro', } break } case 'B': { yearEndBonus = { bonus: 40000, prize: 'mac air', } break } case 'C': { yearEndBonus = { bonus: 20000, prize: 'iphone xr', } break } case 'D': { yearEndBonus = { bonus: 5000, prize: 'ipad mini', } break } } return yearEndBonus }
使用策略模式可以这样:
/** * 获取年终奖 * @param {String} strategyFn 绩效策略函数 * @return {Object} 年终奖,包括奖金和奖品 */ function getYearEndBonus(strategyFn) { if (!strategyFn) { return {} } return strategyFn() } const bonusStrategy = { A() { return { bonus: 50000, prize: 'mac pro', } }, B() { return { bonus: 40000, prize: 'mac air', } }, C() { return { bonus: 20000, prize: 'iphone xr', } }, D() { return { bonus: 10000, prize: 'ipad mini', } }, } const performanceLevel = 'A' getYearEndBonus(bonusStrategy[performanceLevel])
这里每个函数就是一个策略,修改一个其中一个策略,并不会影响其他的策略,都可以单独使用。当然这只是个简单的范例,只为了说明。
策略模式比较明显的特性就是可以减少 if 语句或者 switch 语句。
职责链模式
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
例子
function order(options) { return { next: (callback) => callback(options), } } function order500(options) { const { orderType, pay } = options if (orderType === 1 && pay === true) { console.log('500 元定金预购, 得到 100 元优惠券') return { next: () => {}, } } else { return { next: (callback) => callback(options), } } } function order200(options) { const { orderType, pay } = options if (orderType === 2 && pay === true) { console.log('200 元定金预购, 得到 50 元优惠券') return { next: () => {}, } } else { return { next: (callback) => callback(options), } } } function orderCommon(options) { const { orderType, stock } = options if (orderType === 3 && stock > 0) { console.log('普通购买, 无优惠券') return {} } else { console.log('库存不够, 无法购买') } } order({ orderType: 3, pay: true, stock: 500, }) .next(order500) .next(order200) .next(orderCommon) // 打印出 “普通购买, 无优惠券”
上面的代码,对 order 相关的进行了解耦, order500
, order200
、 orderCommon
等都是可以单独调用的。