1. 工厂模式
定义
用来创建对象的一种最常用的设计模式。不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂
function createUser(role) {
function User(options) {
this.name = options.name
this.viewPage = options.viewPage
}
switch(role){
case 'superAdmin':
return new User({name:'超级管理员',viewPage:['首页','通讯录','发现页','应用数据','权限管理']})
break
case 'admin':
return new User({name:'管理员',viewPage:['首页','通讯录','发现页','应用数据']})
break
case 'user':
return new User({name:'普通用户',viewPage:['首页','通讯录','发现页']})
break
default:
throw new Error('参数错误')
}
}
createUser('admin')
如果要把多个构造函数生成实例的逻辑封装到某个工厂中,也可以将构造函数挂载到工厂函数的原型链上,或者工厂函数的静态方法中:
function createUser(role) {
return new createUser.prototype[role]
}
createUser.prototype.superAdmin = function () {
this.name = '超级管理员'
this.viewPage = ['首页','通讯录','发现页','应用数据','权限管理']
}
createUser.prototype.admin = function () {
this.name = '管理员'
this.viewPage = ['首页','通讯录','发现页','应用数据']
}
createUser.prototype.user = function () {
this.name = '普通用户'
this.viewPage = ['首页','通讯录','发现页']
}
new createUser('admin')
以下几种情景下,开发者应该考虑使用工厂模式:
- 对象的构建十分复杂
- 需要依赖具体环境创建不同实例
- 处理大量具有相同属性的小对象
2. 单例模式
定义
保证一个特定类最多只能有一个实例,意味着第二次使用同一个类创建对象时,应得到和第一次创建对象完全相同
let SingleUser1 = (function () {
let instance = null
return function User() {
if (instance) {
return instance
}
return instance = this
}
})()
对将一个构造函数单例化的逻辑可以进一步封装:
function User(name) {
this.name = name
}
let singleton = function (fn) {
let instance = null
return function (args) {
if (instance) {
return instance
}
return instance = new fn(args)
}
}
let singleUser = new singleton(User)
let user1 = singleUser('zs') // { name : 'zs'}
let user2 = singleUser('ls') // { name : 'zs'}
凡是使用唯一对象的场景,都适用于单例模式,例如登录框,弹窗,遮罩。另外ES6和CommonJS模块化语法中导出的对象也是单例
export default new Vuex.Store({/**/})
例子:生成遮罩
let createMask = singleton(function(){
let mask = document.createElement('div')
mask.style.background = 'red'
mask.style.width = '100%'
mask.style.height = '100%'
mask.style.position = 'fixed'
document.body.appendChild(mask)
return mask
})
3. 适配器模式
将一个类(对象)的接口(方法或属性)转化成用户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以正常工作
示例:
对旧的ajax方法进行迁移改造,由于历史原因,无法一次性移除所有的旧代码,因此需要使用适配模式对原代码进行兼容:
$ = {
ajax(options) {
let { method, url } = options
let axiosOptions = {method, url}
let dataProp = method === 'get' ? 'params' : 'data'
axiosOptions[dataProp] = options.data
return axios(axiosOptions).then((res) => {
options.success && options.success(res.data,res.status,res.request)
}).catch((err) => {
options.error && options.error(err)
})
}
}
4. 装饰器模式
允许向一个现有的对象添加新的功能,同时又不改变其结构。例如手机壳,他并没有改变我们手机原有的功能,比如打电话,听音乐什么的。但却为手机提供了新的功能:防磨防摔等
简单实现
function Phone() {
}
Phone.prototype.makeCall = function () {
console.log('拨通电话')
}
function decorate(target) {
target.prototype.code = function () {
console.log('写代码')
}
return target
}
Phone = decorate(Phone)
const phone = new Phone()
使用装饰器语法改造上面的例子:
function code (target) {
target.prototype.code = function () {
console.log('写代码')
}
}
@code
class Phone {
makeCall () {
console.log('打电话')
}
}
const phone = new Phone()
装饰器不光可以装饰类,还可以装饰方法:
class Math {
@log
add(a, b) {
return a + b
}
}
function log(target, name, descriptor) {
// 此时target是 Math.prototype , name 是方法名,即'add'
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// }
var oldValue = descriptor.value
descriptor.value = function() {
console.log(`Calling ${name} with`, arguments)
return oldValue.apply(this, arguments)
}
return descriptor
}
const math = new Math()
//现在调用add方法,则会触发log功能
math.add(2, 4)
5. 代理模式
为对象提供另一个代理对象以控制对这个对象的访问。
使用代理的原因是我们不想对原对象进行直接操作,而是通过一个“中间人”来传达操作。生活中有许多代理的例子,比如访问某个网站,不想直接访问,通过中间的一台服务器来转发请求,这台服务器就是代理服务器。又比如明星,普通人无法直接联系他们,而是通过经纪人进行联系。
简单实现
let star = {
name:'zs',
age:21,
height:170,
bottomPrice:100000,
announcements:[]
}
let proxy = new Proxy(star,{
get:function (target,key) {
if (key === 'height') {
return target.height + 10
} else if(key === 'announcements') {
return new Proxy(target.announcements,{
set:function (target,key,value) {
if(key !== 'length' && target.length === 3){
console.log('不好意思,今年通告满了')
return true
}
target[key] = value
return true
}
})
} else {
return target[key]
}
},
set:function (target, key, value,) {
if (key === 'price') {
if (value > target.bottomPrice * 1.5) {
console.log('成交');
target.price = value
} else if (value > target.bottomPrice) {
console.log('咱们再商量商量')
} else {
throw new Error('下次说吧')
}
}
}
})
proxy.announcements.push('爸爸去哪儿')
proxy.announcements.push('中国好声音')
proxy.announcements.push('奇葩说')
proxy.announcements.push('快乐大本营')
proxy.price = 160000
proxy.price = 120000
proxy.price = 9000
应用:dom事件代理 。Vue源码
注意区分适配器模式(Adapter),装饰器模式(Decorator),代理模式(Proxy):
适配器模式提供不同的新接口,通常用作接口转换的兼容处理
代理模式提供一模一样的新接口,对行为进行拦截
装饰器模式,直接访问原接口,直接对原接口进行功能上的增强
6. 外观模式
为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易
简单例子
function addEvent(dom, type, fn) {
if (dom.addEventListener) { // 支持DOM2级事件处理方法的浏览器
dom.addEventListener(type, fn, false)
} else if (dom.attachEvent) { // 不支持DOM2级但支持attachEvent
dom.attachEvent('on' + type, fn)
} else {
dom['on' + type] = fn // 都不支持的浏览器
}
}
const myInput = document.getElementById('myinput')
addEvent(myInput, 'click', function() {console.log('绑定 click 事件')})
注意区分工厂模式和外观模式:
工厂模式核心是对创建对象的逻辑进行封装。
外观模式核心是对不同的接口进行封装。
7. 观察者模式
通常又被称为 发布-订阅者模式 (Publisher/Subscribers):它定义了对象和对象间的一种依赖关系,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合
简单实现
class Publisher{
_state = 0;
subscribers = []
get state(){
return this._state
}
set state(value){
this._state = value
this.notify(value)
}
notify(value){
this.subscribers.forEach(subscriber => subscriber.update(value))
}
collect(subscriber){
this.subscribers.push(subscriber)
}
}
let subId = 1
class Subscriber{
publisher = null;
id = subId++;
subscribe(publisher){
this.publisher = publisher
publisher.collect(this)
}
update(value){
console.log(`我是${this.id}号订阅者,收到发布者信息:${value}`);
}
}
let publisher = new Publisher()
let subscriber1 = new Subscriber()
let subscriber2 = new Subscriber()
subscriber1.subscribe(publisher)
subscriber2.subscribe(publisher)
publisher.state = 2
8. 迭代器模式
提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部结构
内部迭代器
在函数内部定义好迭代的规则,完全接手整个迭代的过程,外部只需一次初始调用
let arr = [1, 2, 3, 4, 5, 6, 7, 8]
let each = function(arr, callback){
for(let i=0; i<arr.length; i++){
callback.call(null, i, arr[i]) //把下标和元素当作参数传递给callback参数
}
}
each(arr, function(i, value){
console.log(i, value)
})
内部迭代器在调用时非常方便,但使用回调自由度有限,例如用户想在迭代途中暂停,或者迭代两个数组需要使用each嵌套
外部迭代器
指调用者显式地请求迭代下一个元素,虽然这样做会增加调用的复杂度,但也会增强迭代的操作灵活性
class Iterator {
constructor(list) {
this.list = list
this.index = 0
}
next(){
return {
value:this.list[this.index++],
done:this.index > this.list.length
}
}
}
let it = new Iterator([1,2,3])
ES6语法中提出了迭代器的概念,以下对象默认实现了ES6规定的迭代器接口
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
提供了调用之后能返回迭代器的接口的对象被称为可迭代对象
。ES6中可以用for...of
,扩展运算符(...)
,数组解构,Array.from
等语法迭代。
9. 状态模式
状态模式中是把对象的每种状态都进行封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化
状态模式在生活中很常见:十字路口的交通信号灯,一通电话有拨通,通话,挂断各个状态等等
假设有一个台灯,一开始是关闭状态,按下开关后会变成弱光状态,再次按下按钮会变成强光状态,再次按下按钮会变成关闭状态。如果不使用状态模式:
class Lamp {
constructor() {
this.state = 'off'
}
pressButton() {
if (this.state === 'off') {
console.log('弱光')
this.state = 'weakLight'
} else if (this.state === 'weakLight') {
console.log('强光')
this.state = 'strongLight'
} else if (this.state === 'strongLight') {
console.log('关灯')
this.state = 'off'
}
}
}
这样做存在有如下问题:
- 每次新增或者修改状态名,都需要改动
pressButton
方法中的代码,这使得pressButton
成为了一个非常不稳定的方法。并且这个方法的代码量会迅速膨胀,因为状态改变后的逻辑在实际开发中不仅仅只是例子里的打印 - 状态之间的切换关系,不过是往
pressButton
方法里堆砌if、else语句,增加或者修改一个状态可能需要改变若干个操作,这使pressButton
更加难以阅读和维护
如果使用状态模式,将状态封装成类,在JS中可以这样实现:
class Lamp {
constructor() {
this.offLightState = new OffLightState()
this.weakLightState = new WeakLightState()
this.strongLightState = new StrongLightState()
this.state = this.offLightState
}
pressButton(){
this.state.trigger.call(this)
}
}
class OffLightState {
trigger (){
console.log( '弱光' )
this.state = this.weakLightState
}
}
class WeakLightState {
trigger (){
console.log( '强光' )
this.state = this.strongLightState
}
}
class StrongLightState {
trigger (){
console.log( '关灯' )
this.state = this.offLightState
}
}
如果状态不需要封装成类,可以用普通对象代替:
class Lamp {
constructor() {
this.state = FSM.offLightState
}
pressButton(){
this.state.trigger.call(this)
}
}
//有限状态机
//1. 状态总数是有限的
//2. 任一时刻,只处在一种状态之中
//3. 某种条件下,会从一种状态转变到另外一种状态
const FSM = {
offLightState: {
trigger() {
console.log('弱光')
this.state = FSM.weakLightState
}
},
weakLightState: {
trigger(){
console.log('强光')
this.state = FSM.strongLightState
}
},
strongLightState:{
trigger(){
console.log('关灯');
this.state = FSM.offLightState
}
}
}
以下这些情况中,我们应该考虑状态模式:
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态。
示例:
业务代码:tab栏,货物物流状态,音乐播放器的循环模式,游戏中各种状态等