搬运自 https://github.com/ziyi2/js
# 设计模式
# 概念
# 什么是模式
模式是一种可复用的解决方案,可用于解决软件设计中遇到的常见问题。
# 模式的优点
- 复用模式有助于防止在应用程序开发过程中小问题引发大问题
- 模式可以提供通用的解决方案
- 某些模式可以通过避免代码复用减少代码的总体资源占用量
- 模式会使开发沟通更快速
- 模式可以逐步改进
# 设计模式的结构
- 模式名称
- 描述
- 上下文大纲
- 问题陈述
- 解决方案
- 设计
- 实现
- 插图
- 示例
- 辅助条件
- 关系
- 已知的用法
- 讨论
# 反模式
JavaScript中的反模式示例如下
- 在全局上下文中定义大量的变量污染全局命名空间
- 向setTimeout或setInterval传递字符串,而不是函数
- 修改Object类的原型
- 以内联形式使用JavaScript
- 使用document.write
# 设计模式的类别
# 创建型
专注于处理对象创建机制,以适合给定情况的方式来创建对象。创建对象的基本方法可能导致项目复杂性增加,而创建型模式旨在通过控制创建过程来解决这种问题。
设计模式 | 描述 |
---|---|
Constructor(构造器) | |
Factory(工厂) | |
Abstract(抽象) | |
Prototype(原型) | |
Singleton(单例) | |
Singleton(单例) | |
Builder(生成器) |
# 结构型
结构型与对象组合有关,通常可以用于找出在不同对象之间建立关系的简单方法。这种模式有助于确保在系统某一部分发生变化时,系统的整个结构不需要同时改变,同时对于不适合某一特定目的而改变的系统部分,也能够完成重组。
设计模式 | 描述 |
---|---|
Iterator(装饰者) | |
Facade(外观) | |
Flyweight(享元) | |
Adapter(适配器) | |
Proxy(代理) |
# 行为
行为设计模式专注于改善或简化系统中不同对象之间的通信。
设计模式 | 描述 |
---|---|
Iterator(迭代器) | |
Mediator(中介者) | |
Observer(观察者) | |
Visitor(访问者) |
# Constructor(构造器)模式
function Person() {}
var person = new Person()
// 带原型的Constructor
Person.prototype.getName = {}
# Module(模块)模式
- 对象字面量表示法
- Module模式
- AMD模式
- CommonJS模块
- ECMAScript Harmony模块
# 对象字面量表示法
let person = {
name: '',
age: 0,
getName: function() {},
getAge: function() {}
}
# Module模式
Module模式可以为类提供私有和公有的方法,Module模式会封装一个作用域,从而屏蔽来自全局作用域的特殊部分,使一个单独的对象拥有公有/私有的方法和变量。
Module模式使用闭包封装私有状态和组织,提供一种包装混合公有/私有方法和变量的方式,防止其泄露至全局作用域,防止与全局作用域中的方法和变量发生冲突。通过该模式只需返回一个公有API,而其他的一切则都维持在私有闭包里。
Module模式可以屏蔽处理底层事件逻辑,只暴露供应用程序调用的公有API,该模式返回的是一个对象而不是函数。
// 一个立即执行的匿名函数创建了一个作用域
// 全局作用域无法获取私有变量_counter
var moduleMode = (function() {
// 私有变量
var _counter = 0
// 返回一个公有对象
return {
// 公有API
increment: function() {
return ++_counter
},
// 公有API
reset: function() {
_counter = 0
return _counter
}
}
})()
let counter = moduleMode.increment()
console.log(counter)
counter = moduleMode.increment()
console.log(counter)
counter = moduleMode.reset()
console.log(counter)
// _counter is not defined
console.log(_counter)
Module模式的本质是使用函数作用域来模拟私有变量,在模式内,由于闭包的存在,声明的变量和方法旨在改模式内部可用,但在返回对象上定义的变量和方法,则对外部使用者可用。
Module模式也可用于命名空间
var Namespace = (function() {
// 私有变量
var _counter = 0
// 私有方法
var _sayCounter = function() {
console.log(_counter)
}
// 返回一个公有对象
return {
// 公有变量
counter: 10,
// 公有API
increment: function() {
++_counter
// 调用私有变量
_sayCounter()
return _counter
},
// 公有API
reset: function() {
_counter = 0
_sayCounter()
return _counter
}
}
})()
Namespace.increment()
Namespace.increment()
Namespace.reset()
# Module模式的变化
# 引入
可以使jQuery、Underscore作为参数引入模块
var moduleMode = (function($, _) {
function _method() {
$('.container').html('test')
}
return {
method: function() {
_method()
}
}
})($,_)
moduleMode.method()
# 引出
let moduleMode = (function() {
var public = {},
_private = 'hello'
function _method() {
console.log(_private)
}
public.name = 'public'
public.method = function () {
_method()
}
return public
})()
console.log(moduleMode.name)
# Module模式的优缺点
- 优点:整洁、支持私有数据。
- 缺陷:私有数据难以维护(想改变可见性需要修改每一个使用该私有数据的地方),无法为私有成员创建自动化单元测试,开发人员无法轻易扩展私有方法。
# Singleton(单例/单体)模式
单例模式作为一个静态的实例实现时,可以延迟创建实例,从而在没有使用该静态实例之前,无需占用浏览器的资源或内存。同时当在系统中确实需要一个对象来协调其他对象时,Singleton模式非常有用。单例模式可以推迟初始化,通常是因为它需要一些信息,这些信息在初始化期间可能无法获得。
var Singleton = (function() {
function Single(options) {
options = options || {}
this.name = options.name || 'Single'
this.age = options.age || 1
}
var _instance
// 返回一个闭包,_instance不会被销毁
return {
name: 'Singleton',
getInstance: function(options) {
if(!_instance) {
_instance = new Single(options)
}
return _instance
}
}
})()
var single = Singleton.getInstance({
name: 'ziyi2',
age: 28
})
// Single {name: "ziyi2", age: 28}
console.log(single)
# Observer(观察者)模式
观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变更自动通知给这一系列观察者对象。当subject目标对象需要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。
一个或多个观察者对目标对象的状态感兴趣时,可以将自己依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每个观察者的更新方法。如果观察者对目标对象的状态不感兴趣,也可以将自己从中分离。
对象 | 描述 |
---|---|
Subject(目标) | 维护一系列的观察者,方便添加、删除和通知观察者 |
Observer(观察者) | 为那些目标状态发生改变时需要通知的对象提供一个更新接口 |
subject(目标)实例对象 | 状态发生变化时通知观察者实例对象们更新状态 |
observer(观察者)实例对象 | 实现更新接口用于更新状态 |
# 观察者列表对象
观察者列表对象用于维护一系列的观察者实例对象
// 观察者列表对象
function ObserverList() {
this.observerList = []
}
// 增加观察者实例对象
ObserverList.prototype.add = function(observer) {
return this.observerList.push(observer)
}
// 查看观察者实例对象的数量
ObserverList.prototype.getCount = function() {
return this.observerList.length
}
// 获取某个观察者实例对象
ObserverList.prototype.get = function(index) {
if(index < -1 || index > this.observerList.length) return
return this.observerList[index]
}
// 删改观察者实例对象的列表 省略...
# Subject(目标)对象
使用观察者列表对象维护观察者实例对象,并可以通知观察者实例对象更新状态
// 目标对象(在观察者列表上增、删观察者实例对象)
function Subject() {
this.observers = new ObserverList()
}
// 增加观察者实例对象
Subject.prototype.add = function(observer) {
this.observers.add(observer)
}
// 通知观察者列表更新
Subject.prototype.notify = function(context) {
var count = this.observers.getCount()
for(var i=0; i<count; i++) {
this.observers.get(i).update(context)
}
}
# Observer(观察者)对象
主要用于提供更新接口
function Observer() {
// 更新目标实例对象的状态的接口
this.update = function() {}
}
# 扩展对象
扩展对象用于扩展观察者实例对象和目标实例对象
// obj -> 观察者实例对象或目标实例对象
// extension -> 需要被扩展的对象
function extend(obj, extension) {
for(var key in obj) {
extension[key] = obj[key]
}
}
# 具体的DOM元素用于创建观察者实例对象和目标实例对象
创建DOM元素,用于扩展观察者实例对象和目标实例对象
<button id="btn">添加新的观察者实例对象</button>
<!-- 目标实例对象 -->
<input type="checkbox" id="checkbox">
<!-- 新创建的观察者实例对象的容器 -->
<div id="div"></div>
创建目标实例对象
// 获取checkbox元素
var checkbox = document.getElementById('checkbox')
// 创建具体目标实例,并绑定到checkbox元素上
extend(new Subject(), checkbox)
// 点击checkbox会触发目标实例对象的通知方法
// 广播到所有观察者实例对象促使它们调用更新状态方法
checkbox.onclick = function() {
checkbox.notify(checkbox.checked)
}
创建观察者实例对象
// 获取btn和div元素
var btn = document.getElementById('btn'),
div = document.getElementById('div')
btn.onclick = handlerClick
function handlerClick() {
// 创建checkbox元素(注意和目标实例对象不同)
var input = document.createElement("input")
input.type = "checkbox"
// 创建具体的观察者实例,并绑定到checkbox元素上
extend(new Observer(), input)
// 重写观察者更新行为
input.update = function(value) {
this.checked = value
}
// 通过目标实例对象新增需要被广播的观察者实例对象
checkbox.add(input)
// 将观察者附到div元素上
div.appendChild(input)
}
至此,通过按钮新增观察者实例对象,点击目标checkbox实例对象时,checkbox的状态会广播给所有新增的观察者实例对象checkbox,从而使目标实例对象的值和观察者实例对象的值保持一致,实现了观察者模式。
# Publish/Subscribe(发布/订阅)模式
需要注意token是每一次订阅的唯一标识,通过token可以取消特定的频道订阅。
// 发布/订阅模式
var pubsub = (function() {
// 订阅和发布的事件频道集(桥梁、中间带)
var _channels = [],
_subUid = -1
return {
// 订阅频道
subscribe: function(channel, handler) {
if(!_channels[channel]) _channels[channel] = []
var token = (++_subUid).toString()
_channels[channel].push({
token: token,
handler: handler
})
return token
},
// 广播频道
publish: function(channel, data) {
if(!_channels[channel]) return false
// 获取频道订阅者
var subscribers = _channels[channel]
var len = subscribers.length
// 后订阅先触发
while(len--) {
subscribers[len].handler(data, channel, subscribers[len].token)
}
return this
},
// 移除订阅
unsubscribe: function(token) {
for(var channel in _channels) {
var len = _channels[channel].length
for(var index=0; index<len; index++) {
if(_channels[channel][index].token === token) {
_channels[channel].splice(index, 1)
return token
}
}
}
}
}
})()
// 订阅频道0
var token = pubsub.subscribe('channel0', function(data, channel, token) {
console.log('channel: ' + channel +' data: ', data + ' token: ' + token)
})
// 订阅频道0
pubsub.subscribe('channel0', function(data, channel, token) {
console.log('channel: ' + channel +' data: ', data + ' token: ' + token)
})
// 广播频道0
pubsub.publish('channel0', {
name: 'ziyi2',
age: 28
})
// 取消某个特定频道0的订阅
pubsub.unsubscribe(token)
// 继续广播频道0
pubsub.publish('channel0', {
name: 'ziyi2',
age: 28
})
# Observer(观察者)模式和Publish/Subscribe(发布/订阅)模式的区别
Publish/Subscribe(发布/订阅)模式使用一个主题/事件通道,这个通道介于订阅者和发布者之间,该设计模式允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者需要的信息,采用事件通道可以避免发布者和订阅者之间产生依赖关系。
需要注意的是,Observer(观察者)模式允许观察者实例对象(订阅者)执行适当的事件处理程序来注册和接收目标实例对象(发布者)发出的通知(即在观察者实例对象上注册update方法),使订阅者和发布者之间产生了依赖关系,且没有事件通道。
# 优点
解耦,可用于设计分层以及分层之间的通信,可用于将应用程序分解为更小、更松散耦合的块,改进代码的管理和潜在复用,是设计解耦