javascript设计模式总结
前端喜哥 人气:0一、设计模式介绍
什么是设计模式
- 设计模式是解决问题的一种思想,和语言无关。在面向对象软件设计的工程中,针对特定的问题简洁优雅的一种解决方案。通俗一点的说,设计模式就是符合某种场景下某个问题的解决方案,通过设计模式可以增加代码的可重用性,可扩展性,可维护性,最终使得我们的代码高内聚、低耦合。
设计模式的五大设计原则
- 单一职责:一个程序只需要做好一件事。如果功能过于复杂就拆分开,保证每个部分的独立
- 开放封闭原则:对扩展开放,对修改封闭。增加需求时,扩展新代码,而不是修改源代码。这是软件设计的终极目标。
- 里氏置换原则:子类能覆盖父类,父类能出现的地方子类也能出现。
- 接口独立原则:保持接口的单一独立,避免出现“胖接口”。这点目前在TS中运用到。
- 依赖导致原则:面向接口编程,依赖于抽象而不依赖于具体。使用方只专注接口而不用关注具体类的实现。俗称“鸭子类型”
设计模式的三大类
- 创建型:工厂模式,抽象工厂模式,建造者模式,单例模式,原型模式
- 结构型:适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式
- 行为型:策略模式,模板方法模式,发布订阅模式,迭代器模式,职责链模式,命令模式,备忘录模式,状态模式,访问者模式,中介者模式,解释器模式。
二、设计模式
1.工厂模式
- 工厂模式是用来创建对象的常见设计模式,在不暴露创建对象的具体逻辑,而是将逻辑进行封装,那么它就可以被称为工厂。工厂模式又叫做静态工厂模式,由一个工厂对象决定创建某一个类的实例。
优点
- 调用者创建对象时只要知道其名称即可
- 扩展性高,如果要新增一个产品,直接扩展一个工厂类即可。
- 隐藏产品的具体实现,只关心产品的接口。
缺点
- 每次增加一个产品时,都需要增加一个具体类,这无形增加了系统内存的压力和系统的复杂度,也增加了具体类的依赖
例子
- 一个服装厂可以生产不同类型的衣服,我们通过一个工厂方法类来模拟产出
class DownJacket { production(){ console.log('生产羽绒服') } } class Underwear{ production(){ console.log('生产内衣') } } class TShirt{ production(){ console.log('生产t恤') } } // 工厂类 class clothingFactory { constructor(){ this.downJacket = DownJacket this.underwear = Underwear this.t_shirt = TShirt } getFactory(clothingType){ const _production = new this[clothingType] return _production.production() } } const clothing = new clothingFactory() clothing.getFactory('t_shirt')// 生产t恤
2.抽象工厂模式
- 抽象工厂模式就是通过类的抽象使得业务适用于一个产品类簇的创建,而不负责某一个类产品的实例。抽象工厂可以看作普通工厂的升级版,普通工厂以生产实例为主,而抽象工厂的目就是生产工厂。
优点
- 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点
- 产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。
例子
- 同样基于上面的例子,模拟出一个抽象类,同时约束继承子类的方法实现。最后再通过工厂函数返回指定的类簇
/* 抽象类 js中abstract是个保留字,实现抽象类只能通过new.target进行验证, 防止抽象类被直接实例,另外如果子类没有覆盖指定方法,则抛出错误 */ class ProductionFlow { constructor(){ if(new.target === ProductionFlow){ throw new Error('抽象类不能被实例') } } production(){ throw new Error('production要被重写') } materials(){ throw new Error('materials要被重写') } } class DownJacket extends ProductionFlow{ production(){ console.log(`材料:${this.materials()},生产羽绒服`) } materials(){ return '鸭毛' } } class Underwear extends ProductionFlow{ production(){ console.log(`材料:${this.materials()},生产内衣`) } materials(){ return '丝光棉' } } class TShirt extends ProductionFlow{ production(){ console.log(`材料:${this.materials()},生产t恤`) } materials(){ return '纯棉' } } function getAbstractProductionFactory(clothingType){ const clothingObj = { downJacket:DownJacket, underwear:Underwear, t_shirt:TShirt, } if(clothingObj[clothingType]){ return clothingObj[clothingType] } throw new Error(`工厂暂时不支持生产这个${clothingType}类型的服装`) } const downJacketClass = getAbstractProductionFactory('downJacket') const underwearClass = getAbstractProductionFactory('underwear') const downJacket = new downJacketClass() const underwear = new underwearClass() downJacket.production() // 材料:鸭毛,生产羽绒服 underwear.production() // 材料:丝光棉,生产内衣
3.建造者模式
- 建造者模式是一种比较复杂使用频率较低的创建型设计模式,建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。主要用于将一个复杂对象的构建与他的表现分离,使得同样的构建过程可以创建不同的表示。
优点
- 建造者独立易扩展
- 方便控制细节风险
缺点
- 产品必须有共同点,范围有限制
- 当内部有变化复杂时,会有很多建造类
例子
下面继续用服装厂的生产流程作为例子。
// 抽象类 class Clothing { constructor() { this.clothingType = '' this.price } } class Underwear extends Clothing { constructor() { super() this.clothingType = 'underwear' this.price = 10 } } class TShirt extends Clothing { constructor() { super() this.clothingType = 't_shirt' this.price = 50 } } class DownCoat extends Clothing { constructor() { super() this.clothingType = 'DownCoat' this.price = 500 } } // 产品 class Purchase { constructor() { this.clothings = [] } addClothing(clothing) { this.clothings.push(clothing) } countPrice() { return this.clothings.reduce((prev, cur)=>cur.price + prev,0) } } // 厂长 class FactoryManager { createUnderwear() { throw new Error(`子类必须重写 createUnderwear`) } createTShirt() { throw new Error(`子类必须重写 createTShirt`) } createDownCoat() { throw new Error(`子类必须重写 DownCoat`) } } // 工人 class Worker extends FactoryManager { constructor() { super() this.purchase = new Purchase() } createUnderwear(num) { for (let i = 0; i < num; i++) { this.purchase.addClothing(new Underwear()) } } createTShirt(num) { for (let i = 0; i < num; i++) { this.purchase.addClothing(new TShirt()) } } createDownCoat(num) { for (let i = 0; i < num; i++) { this.purchase.addClothing(new DownCoat()) } } } // 销售 class Salesman { constructor() { this.worker = null } setWorker(worker) { this.worker = worker } reserve(clothing) { clothing.forEach((item) => { if (item.type === 'underwear') { this.worker.createUnderwear(item.num) } else if (item.type === 't_shirt') { this.worker.createTShirt(item.num) } else if (item.type === 'DownCoat') { this.worker.createDownCoat(item.num) } else { try { throw new Error('公司暂不生产或不存在该类型的商品') } catch (error) { console.log(error) } } }); const purchase = this.worker.purchase return purchase.countPrice() } } const salesman = new Salesman() const worker = new Worker() salesman.setWorker(worker) const order = [ { type: 'underwear', num: 10 }, { type: 't_shirt', num: 4 }, { type: 'DownCoat', num: 1 } ] console.log(`本次订单所需金额:${salesman.reserve(order)}`)
4.单例模式
- 单例模式的思路是:保证一个类只能被实例一次,每次获取的时候,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回。
- 单例模式的核心就是创建一个唯一的对象,而在javascript中创建一个唯一的对象太简单了,为了获取一个对象而去创建一个类有点多此一举。如
const obj = {}
,obj
就是独一无二的一个对象,在全局作用域的声明下,可以在任何地方对它访问,这就满足了单例模式的条件。
优点
- 内存中只有一个实例,减少了内存的开销。
- 避免了对资源多重的占用。
缺点
- 违反了单一职责,一个类应该只关心内部逻辑,而不用去关心外部的实现
例子
- 我们常见到的登录弹窗,要么显示要么隐藏,不可能同时出现两个弹窗,下面我们通过一个类来模拟弹窗。
class LoginFrame { static instance = null constructor(state){ this.state = state } show(){ if(this.state === 'show'){ console.log('登录框已显示') return } this.state = 'show' console.log('登录框展示成功') } hide(){ if(this.state === 'hide'){ console.log('登录框已隐藏') return } this.state = 'hide' console.log('登录框隐藏成功') } // 通过静态方法获取静态属性instance上是否存在实例,如果没有创建一个并返回,反之直接返回已有的实例 static getInstance(state){ if(!this.instance){ this.instance = new LoginFrame(state) } return this.instance } } const p1 = LoginFrame.getInstance('show') const p2 = LoginFrame.getInstance('hide') console.log(p1 === p2) // true
5.适配器模式
- 适配器模式的目的是为了解决对象之间的接口不兼容的问题,通过适配器模式可以不更改源代码的情况下,让两个原本不兼容的对象在调用时正常工作。
优点
- 让任何两个没有关联的类可以同时有效运行,并且提高了复用性、透明度、以及灵活性
缺点
- 过多的使用适配器模式,会让系统变得零乱,不易整体把控。建议在无法重构的情况下使用适配器。
例子
- 拿一个现实中的例子来说,杰克只会英语,小明只会中文,它们在交流上出现了障碍,小红同时会中英双语,通过小红将杰克的英语翻译成中文,让小明和杰克进行无障碍的沟通,这里小红就起到了适配器的角色。
class Jack { english() { return 'I speak English' } } class Xiaoming { chinese() { return '我只会中文' } } // 适配器 class XiaoHong { constructor(person) { this.person = person } chinese() { return `${this.person.english()} 翻译: "我会说英语"` } } class Communication { speak(language) { console.log(language.chinese()) } } const xiaoming = new Xiaoming() const xiaoHong = new XiaoHong(new Jack()) const communication = new Communication() communication.speak(xiaoming) communication.speak(xiaoHong)
6.装饰器模式
- 装饰者模式能够在不更改源代码自身的情况下,对其进行职责添加。相比于继承装饰器的做法更轻巧。通俗的讲我们给心爱的手机上贴膜,带手机壳,贴纸,这些就是对手机的装饰。
优点
- 装饰类和被装饰类它们之间可以相互独立发展,不会相互耦合,装饰器模式是继承的一个替代模式,它可以动态的扩展一个实现类的功能。
缺点
- 多层的装饰会增加复杂度
例子
- 在编写飞机大战的游戏中,飞机对象的攻击方式只有普通子弹攻击,如何在不更改原代码的情况下,为它其他的攻击方式,如激光武器,导弹武器?
class Aircraft { ordinary(){ console.log('发射普通子弹') } } class AircraftDecorator { constructor(aircraft){ this.aircraft = aircraft } laser(){ console.log('发射激光') } guidedMissile(){ console.log('发射导弹') } ordinary(){ this.aircraft.ordinary() } } const aircraft = new Aircraft() const aircraftDecorator = new AircraftDecorator(aircraft) aircraftDecorator.ordinary() // 发射普通子弹 aircraftDecorator.laser() // 发射激光 aircraftDecorator.guidedMissile() // 发射导弹 // 可以看到在不更改源代码的情况下对它进行了装饰扩展
7.代理模式
- 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
- 代理和本体接口需要一致性,代理和本体之间可以说是鸭子类型的关系,不在乎他怎么实现的,只要它们之间暴露的方法一致既可。
优点
- 职责清晰,高扩展性,智能化
缺点
- 当对象和对象之间增加了代理可能会影响到处理的速度。
- 实现代理需要额外的工作,有些代理会非常的复杂。
例子
- 我们都知道,领导拥有公司的最高权限,假设公司有员工100个,如果每个人都去找领导去处理事务,那领导肯定会崩溃,因此领导招聘了一个秘书帮他收集整理事务,秘书会在合适时间一次性将需要处理的业务交给老板处理,在这里秘书就是领导的一个代理角色。
// 员工 class Staff { constructor(affairType){ this.affairType = affairType } applyFor(target){ target.receiveApplyFor(this.affairType) } } // 秘书 class Secretary { constructor(){ this.leader = new Leader() } receiveApplyFor(affair){ this.leader.receiveApplyFor(affair) } } //领导 class Leader { receiveApplyFor(affair){ console.log(`批准:${affair}`) } } const staff = new Staff('升职加薪') staff.applyFor(new Secretary()) // 批准:升职加薪
8.外观模式
- 外观模式本质就是封装交互,隐藏系统的复杂性,提供一个可以访问的接口。由一个将子系统一组的接口集成在一起的高层接口,以提供一个一致的外观,减少外界与多个子系统之间的直接交互,从而更方便的使用子系统。
优点
- 减少系统的相互依赖,以及安全性和灵活性
缺点
- 违反开放封闭原则,有变动的时候更改会非常麻烦,即使继承重构都不可行。
例子
- 外观模式经常被用于处理高级游览器的和低版本游览器的一些接口的兼容处理
function addEvent(el,type,fn){ if(el.addEventlistener){// 高级游览器添加事件DOM API el.addEventlistener(type,fn,false) }else if(el.attachEvent){// 低版本游览器的添加事件API el.attachEvent(`on${type}`,fn) }else {//其他 el[type] = fn } }
- 另一种场景,在某个函数中的某个参数可传可不传的情况下,通过函数重载的方式,让传参更灵活。
function bindEvent(el,type,selector,fn){ if(!fn){ fn = selector } // 其他代码 console.log(el,type,fn) } bindEvent(document.body,'click','#root',()=>{}) bindEvent(document.body,'click',()=>{})
9.发布订阅模式
- 发布订阅又称观察者模式,它定义对象之间的1对N的依赖关系,当其中一个对象发生变化时,所有依赖于它的对象都会得到通知。
- 发布订阅模式经常出现在我们的工作场景中,如:当你给DOM绑定一个事件就已经使用了发布订阅模式,通过订阅DOM上的click事件,当被点击时会向订阅者发布消息。
优点
- 观察者和被观察者它们之间是抽象耦合的。并且建立了触发机制。
缺点
- 当订阅者比较多的时候,同时通知所有的订阅者可能会造成性能问题。
- 在订阅者和订阅目标之间如果循环引用执行,会导致崩溃。
- 发布订阅模式没有办法提供给订阅者所订阅的目标它是怎么变化的,仅仅只知道它变化了。
例子
- 比喻前段时间的冬奥会,项目还没有开始的时候可以提前预定,等到项目快开始的时,APP会提前给我们发送通知即将开始的项目,而没到时间的不通知,另外在项目还没有开始的时候,可以取消订阅避免接受到通知。根据这个需求我们来写一个例子吧
class Subject { constructor(){ this.observers = {} this.key = '' } add(observer){ const key = observer.project if (!this.observers[key]) { this.observers[key] = [] } this.observers[key].push(observer) } remove(observer){ const _observers = this.observers[observer.project] console.log(_observers,11) if(_observers.length){ _observers.forEach((item,index)=>{ if(item === observer){ _observers.splice(index,1) } }) } } setObserver(subject){ this.key = subject this.notifyAllObservers() } notifyAllObservers(){ this.observers[this.key].forEach((item,index)=>{ item.update() }) } } class Observer { constructor(project,name) { this.project = project this.name = name } update() { console.log(`尊敬的:${this.name} 你预约的项目:【${this.project}】 马上开始了`) } } const subject = new Subject() const xiaoming = new Observer('滑雪','xiaoming') const A = new Observer('大跳台','A') const B = new Observer('大跳台','B') const C = new Observer('大跳台','C') subject.add(xiaoming) subject.add(A) subject.add(B) subject.add(C) subject.remove(B) // 取消订阅 subject.setObserver('大跳台') /** 执行结果 * 尊敬的:A 你预约的项目:【大跳台】 马上开始了 * 尊敬的:C 你预约的项目:【大跳台】 马上开始了 */
10.迭代器模式
- 迭代器模式是指提供一种方法顺序访问一个聚合对象中的每个元素,并且不需要暴露该对象的内部。
优点
- 它支持以不同的方式遍历一个聚合对象。
- 迭代器简化了聚合类。在同一个聚合上可以有多个遍历。
- 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
例子
迭代器分为内部迭代器和外部迭代器,它们有各自的适用场景。
- 内部迭代器
// 内部迭代器表示内部已经定义好了迭代规则,它完全接受整个迭代过程,外部只需一次初始调用。 Array.prototype.MyEach = function(fn){ for(let i = 0;i<this.length;i++){ fn(this[i],i,this) } } Array.prototype.MyEach = function(fn){ for(let i = 0;i<this.length;i++){ fn(this[i],i,this) } } [1,2,3,4].MyEach((item,index)=>{ console.log(item,index) })
- 外部迭代器
// 外部迭代器必须显示的迭代下一个元素。它增加了调用的复杂度,但也增加了迭代器的灵活性,可以手动控制迭代的过程。 class Iterator{ constructor(arr){ this.current = 0 this.length = arr.length this.arr = arr } next(){ return this.getCurrItem() } isDone(){ return this.current>=this.length } getCurrItem(){ return { done:this.isDone(), value:this.arr[this.current++] } } } let iterator =new Iterator([1,2,3]) while(!(item=iterator.next()).done) { console.log(item) } iterator.next() /* 下面的数据格式是不是有点熟悉 {done: false, value: 1} {done: false, value: 2} {done: false, value: 3} {done: true, value: undefined} */
11.状态模式
- 允许一个对象在其内部状态改变的时候改变其行为,对象看起来似乎修改了它的类,通俗一点的将就是记录一组状态,每个状态对应一个实现,实现的时候根据状态去运行实现。
优点
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好,对切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
例子
- lol中的瑞文的Q有三段攻击,同一个按键,在不同的状态下,攻击的行为不同。通常情况下,我们通过if...else也可以实现,但是这样明显不利于扩展,违反了开放封闭原则。接下来用代码来描述这种场景。
class State { constructor(attack){ this.attack = attack } handle(context){ console.log(this.attack) context.setState(this) } } class Context { constructor(){ this.state = null } getState(){ return this.state } setState(state){ this.state = state } } const q1 = new State('q1 第1击'), q2 = new State('q2 第2击'), q3 = new State('q3 第3击'), context = new Context() q1.handle(context)//q1 第1击 q2.handle(context)//q2 第2击 q3.handle(context)//q3 第3击
12.策略模式
- 策略模式指的是定义一系列算法,把他们一个个封装起来,目的就是将算法的使用和算法的实现分离开来。同时它还可以用来封装一系列的规则,比如常见的表单验证规则,只要这些规则指向的目标一致,并且可以被替换使用,那么就可以用策略模式来封装它们。
优点
- 算法可以自由切换,避免了使用多层条件判断,增加了扩展性
缺点
- 策略类增多,所有策略类都需要对外暴露。
例子
- 刚入这个行业的时候,写表单验证经常无止境的if...else写法,意识到这种写法不靠谱,于是我把检验规则放在一个对象中,在函数中对它进行控制,把规则与实现进行了分离,每次只需要在封装的规则中去修改配置。在后面的多种场景都用这种方法,解决了频繁使用if...else的问题,当第一次接触倒策略模式才知道这种写法也算策略模式。
const rules = { cover_img: { must: false, msg: '请上传封面图片', val: '' }, name: { must: true, msg: '姓名不能为空', val: '' }, sex: { must: true, msg: '请填写性别', val: '' }, birthday: { must: false, msg: '请选择生日', val: '' }, } function verify(){ for(const key in rules){ if(rules[key].must&&!rules[key].val){ console.log(rules[key].msg) } } } verify() // 姓名不能为空 // 请填写性别
- 上面的例子是以js方式写的,在javascript将函数作为一等公民的语言里,策略模式就是隐形的,它已经融入到了javascript的语言中,所以以javascript方式的策略模式会显得简单直接。不过我们依然要了解传统的策略模式,下面来看看传统的策略模式的例子。
//html----------------- <form action="http:// xxx.com/register" id="registerForm" method="post"> 请输入用户名:<input type="text" name="userName" /> 请输入密码:<input type="text" name="password" /> 请输入手机号码:<input type="text" name="phoneNumber" /> <button>提交</button> </form> // js------------------ class Strategies { constructor() { this.rules = {} } add(key, rule) { this.rules[key] = rule return this } } class Validator { constructor(strategies) { this.cache = [] // 保存检验规则 this.strategies = strategies } add(dom, rules) { rules.forEach((rule) => { const strategyAry = rule.strategy.split(':') this.cache.push(() => { const strategy = strategyAry.shift() strategyAry.unshift(dom.value) strategyAry.push(rule.errorMsg) console.log(this.strategies[strategy]) return this.strategies[strategy].apply(dom, strategyAry) }) }); } start() { for (let i = 0,validatorFunc; validatorFunc =this.cache[i++]; ) { const msg = validatorFunc() if (msg) { return msg } } } } const registerForm = document.getElementById('registerForm') // 获取formDom节点 const strategies = new Strategies() strategies.add('isNonEmpty', function(value, errorMsg) { if (!value) { return errorMsg } }).add('minLength', function(value, length, errorMsg) { if (value.length < length) { return errorMsg } }).add('isMobile', function(value, errorMsg) { if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg } }) function validataFunc() { const validator = new Validator(strategies.rules) // 多个校验规则 validator.add(registerForm.userName, [ { strategy: 'isNonEmpty', errorMsg: '用户名不能为空' }, { strategy: 'minLength:10', errorMsg: '用户名长度不能少于10位' } ]) validator.add(registerForm.password, [{ strategy: 'minLength:6', errorMsg: '密码长度不能少于6位' }]) validator.add(registerForm.phoneNumber, [{ strategy: 'isMobile', errorMsg: '手机号码格式不对' }]) const errorMsg = validator.start() return errorMsg // 返回错误信息。 } registerForm.onsubmit = function () { const errorMsg = validataFunc() if (errorMsg) { // 如果存在错误信息,显示错误信息,并且阻止onsubmit默认事件 console.log(errorMsg) return false } }
13.命令模式
- 命令模式中的命令指的是一个执行某些特定的事情的指令。
- 命令模式最常见的应用场景如:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时可以通过一种松耦合的方式来设计程序,使得请求发送者和请求接收者消除彼此之间的耦合关系。
优点
- 降低了代码的耦合度,易扩展,出现新的命令可以很容易的添加进去
缺点
- 命令模式使用过度会导致代码中存在过多的具体命令。
例子
- 假设在一个项目中开发某个页面,其中某个程序员负责绘制静态页面,包括某些按钮,而另一个程序员负责开发这几个按钮的具体行为。负责静态页面的程序员暂时不知道这些按钮未来会发生什么,在不知道具体行为是什么作什么的情况下,通过命令模式的帮助,解开按钮和负责具体行为对象之间的耦合。
// html------------------- <button id="button2">点击按钮 1</button> <button id="button2">点击按钮 2</button> <button id="button3">点击按钮 3</button> // js--------------------- const button1 = document.getElementById('button1'), button2 = document.getElementById('button2'), button3 = document.getElementById('button3'); const MenBar = { refresh:function(){ console.log('刷新菜单目录') } } const SubMenu = { add:function(){ console.log('增加子菜单') }, del:function(){ console.log('删除子菜单') } } function setCommand(el,command){ el.onclick = function(){ command.execute() } } class MenuBarCommand{ constructor(receiver,key){ this.receiver = receiver this.key = key } execute(){ this.receiver[this.key]() } } setCommand(button1,new MenuBarCommand(MenBar,'refresh')) setCommand(button2,new MenuBarCommand(SubMenu,'add')) setCommand(button3,new MenuBarCommand(SubMenu,'del'))
14.组合模式
- 组合模式就是由一些小的子对象构建出的更大的对象,而这些小的子对象本身可能也是由多个孙对象组合而成的。
- 组合模式将对象组合成树状结构,以表示“部分-整体”的层次结构。除了用来表示树状结构之外,组合模式的另一个好处就是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
优点
- 高层模块调用简单,节点可以自由添加
缺点
- 其叶对象和子对象声明都是实现类,而不是接口,这违反了依赖倒置原则
例子
- 以我们最常见的文件夹和文件的关系,非常适合用组合模式来描述,文件夹可以包括子文件夹和文件,文件不能包括任何文件,这种关系让最终会形成一棵树。下面来实现文件的添加,扫描该文件里的文件,并且可以删除文件。
// 文件夹类 class Folder { constructor(name) { this.name = name this.parent = null; this.files = [] } // 添加文件 add(file) { file.parent = this this.files.push(file) return this } // 扫描文件 scan() { console.log(`开始扫描文件夹:${this.name}`) this.files.forEach(file => { file.scan() }); } // 删除指定文件 remove() { if (!this.parent) { return } for (let files = this.parent.files, i = files.length - 1; i >= 0; i--) { const file = files[i] if (file === this) { files.splice(i, 1) break } } } } // 文件类 class File { constructor(name) { this.name = name this.parent = null } add() { throw new Error('文件下面不能添加任何文件') } scan() { console.log(`开始扫描文件:${this.name}`) } remove() { if (!this.parent) { return } for (let files = this.parent.files, i = files.length - 1; i >= 0; i++) { const file = files[i] if (file === this) { files.splice(i, 1) } } } } const book = new Folder('电子书') const js = new Folder('js') const node = new Folder('node') const vue = new Folder('vue') const js_file1 = new File('javascript高级程序设计') const js_file2 = new File('javascript忍者秘籍') const node_file1 = new File('nodejs深入浅出') const vue_file1 = new File('vue深入浅出') const designMode = new File('javascript设计模式实战') js.add(js_file1).add(js_file2) node.add(node_file1) vue.add(vue_file1) book.add(js).add(node).add(vue).add(designMode) book.remove() book.scan()
15.模块方法模式
- 模块方法模式是一种基于继承的设计模式,在javascript中没有真正意义上的继承,所有继承都来自原型(prototype)上的继承,随着ES6的class到来,实现了继承的“概念”,让我们可以以一种很方便简洁的方式继承,但其本质上还是原型继承。
- 模板方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。抽象父类主要封装了子类的算法框架,以及实现了一些公共的方法和其他方法的执行顺序。子类通过继承父类,继承了父类的算法框架,并进行重写。
优点
- 提供公共的代码便于维护。行为由父类控制,具体由子类来实现。
缺点
- 其每一个具体实现都需要继承的子类来实现,这无疑导致类的个数增加,使得系统庞大。
例子
- 拿咖啡和茶的例子来说,制作咖啡和茶都需要烧开水,把水煮沸是一个公共方法,随后的怎么冲泡,把什么倒进杯子,以及添加什么配料,它们可能各不一样,根据以上特点,开始我们的例子。
// 抽象父类 class Beverage { boilWater(){ console.log('把水煮沸') } brew(){ throw new Error('字类必须重写brew方法') } pourInCup(){ throw new Error('字类必须重写pourInCup方法') } addCondiments(){ throw new Error('字类必须重写addCondiments方法') } init(){ this.boilWater() this.brew() this.pourInCup() this.addCondiments() } } // 咖啡类 class Coffee extends Beverage { brew(){ console.log('用沸水冲泡咖啡') } pourInCup(){ console.log('把咖啡倒进杯子') } addCondiments(){ console.log('加糖和牛奶') } } // 茶类 class Tea extends Beverage { brew(){ console.log('用沸水侵泡茶叶') } pourInCup(){ console.log('把茶倒进杯子') } addCondiments(){ console.log('加柠檬') } } const coffee = new Coffee() coffee.init() const tea = new Tea() tea.init()
16.享元模式
- 享元模式是一种用于性能优化的模式,核心是运用共享技术来有效支持大量的细粒度对象。如果系统中创建了大量的类似对象,会导致内存消耗过高,通过享用模式处理重用类似对象,减少内存消耗的问题,达到性能优化方案。
- 享元模式的关键是如何区分内部状态和外部状态
- 内部状态:可以被对象共享,通常不会改变的称为内部状态
- 外部状态:取决于具体的场景,根据具体的场景变化,并且不能被共享的称为外部状态
优点
- 减少了大批量对象的创建,降低了系统了内存。
缺点
- 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
例子
let id = 0 // 定义内部状态 class Upload { constructor(uploadType) { this.uploadType = uploadType } // 点击删除时 小于3000直接删除,大于3000通过confirm提示弹窗删除。 delFile(id) { uploadManager.setExternalState(id,this) if(this.fileSize < 3000){ return this.dom.parentNode.removeChild(this.dom) } if(window.confirm(`确定要删除该文件吗?${this.fileName}`)){ return this.dom.parentNode.removeChild(this.dom) } } } // 外部状态 class uploadManager { static uploadDatabase = {} static add(id, uploadType, fileName, fileSize) { const filWeightObj = UploadFactory.create(uploadType) const dom = this.createDom(fileName, fileSize, () => { filWeightObj.delFile(id) }) this.uploadDatabase[id] = { fileName, fileSize, dom } } // 创建DOM 并且为button绑定删除事件。 static createDom(fileName, fileSize, fn) { const dom = document.createElement('div') dom.innerHTML = ` <span>文件名称:${fileName},文件大小:${fileSize}</span> <button class="delFile">删除</button> ` dom.querySelector('.delFile').onclick = fn document.body.append(dom) return dom } static setExternalState(id, flyWeightObj) { const uploadData = this.uploadDatabase[id] for (const key in uploadData) { if (Object.hasOwnProperty.call(uploadData, key)) { flyWeightObj[key] = uploadData[key] } } } } // 定义一个工厂创建upload对象,如果其内部状态实例对象存在直接返回,反之创建保存并返回。 class UploadFactory { static createFlyWeightObjs = {} static create(uploadType) { if (this.createFlyWeightObjs[uploadType]) { return this.createFlyWeightObjs[uploadType] } return this.createFlyWeightObjs[uploadType] = new Upload(uploadType) } } // 开始加载 const startUpload = (uploadType, files)=>{ for (let i = 0, file; file = files[i++];) { uploadManager.add(++id, uploadType, file.fileName, file.fileSize) } } startUpload('plugin', [ {fileName: '1.txt',fileSize: 1000}, {fileName: '2.html',fileSize: 3000}, {fileName: '3.txt',fileSize: 5000} ]); startUpload('flash', [ {fileName: '4.txt',fileSize: 1000}, {fileName: '5.html',fileSize: 3000}, {fileName: '6.txt',fileSize: 5000} ]);
17.职责链模式
- 职责链模式的定义是:使用多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象链成一条链,并沿着这条链传递该请求,知道有一个对象处理它为止。
优点
- 降低耦合度,它将请求的发送者和接收者解耦。
- 简化了对象,使得对象不需要知道链的结构。
- 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
缺点
- 不能保证每一条请求都一定被接收。
- 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
- 可能不容易观察运行时的特征,有碍于排除问题。
例子
- 假设我们负责一个手机售卖的电商网站,分别缴纳500元和200元定金的两轮预订后,会分别收到100元和50元的优惠券,而没有支付定金的则视为普通购买,没有优惠券,并且在库存有限的情况下也无法保证能购买到。
class Order500 { constructor(){ this.orderType = 1 } handle(orderType, pay, stock){ if(orderType === this.orderType&&pay){ console.log('500元定金预约,得到100元优惠券') }else { return 'nextSuccessor' } } } class Order200 { constructor(){ this.orderType = 2 } handle(orderType, pay, stock){ if(orderType === this.orderType&&pay){ console.log('200元订金预约,得到50元优惠卷') }else { return 'nextSuccessor' } } } class OrderNormal { constructor(){ this.stock = 0 } handle(orderType, pay, stock){ if (stock > this.stock) { console.log('普通购买,无优惠卷') } else { console.log('手机库存不足') } } } class Chain { constructor(order){ this.order = order this.successor = null } setNextSuccessor(successor){ return this.successor = successor } passRequest(...val){ const ret = this.order.handle.apply(this.order,val) if(ret === 'nextSuccessor'){ return this.successor&&this.successor.passRequest.apply(this.successor,val) } return ret } } console.log(new Order500()) var chainOrder500 = new Chain( new Order500() ); var chainOrder200 = new Chain( new Order200() ); var chainOrderNormal = new Chain( new OrderNormal() ); chainOrder500.setNextSuccessor( chainOrder200 ); chainOrder200.setNextSuccessor( chainOrderNormal ); chainOrder500.passRequest( 1, true, 500 ); // 输出:500 元定金预购,得到 100 优惠券 chainOrder500.passRequest( 2, true, 500 ); // 输出:200 元定金预购,得到 50 优惠券 chainOrder500.passRequest( 3, true, 500 ); // 输出:普通购买,无优惠券 chainOrder500.passRequest( 1, false, 0 ); // 输出:手机库存不足
18.中介模式
- 中介者模式的作用就是解除对象与对象之间的紧密耦合关系。增加一个中介者对象之后,所有相关对象都通过中介者对象来通信,而不是相互引用,所以当一个对象发生改变时,只需要通过中介者对象即可。中介者使各对象之间耦合松散,而且可以独立改变他们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
优点
- 降低了类的复杂度,将一对多转化成了一对一。各个类之间的解耦。
缺点
- 当中介者变得庞大复杂,导致难以维护。
例子
// html----------- 选择颜色:<select name="" id="colorSelect"> <option value="">请选择</option> <option value="red">红色</option> <option value="blue">蓝色</option> </select> <br /> 选择内存:<select name="" id="memorySelect"> <option value="">请选择</option> <option value="32G">32G</option> <option value="63G">64G</option> </select> <br /> 输入购买数量:<input type="text" id="numberInput" /> <br /> <div>你选择了颜色:<span id="colorInfo"></span></div> <div>你选择了内存:<span id="memoryInfo"></span></div> <div>你选择了数量:<span id="numberInfo"></span></div> <button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button> // js ------------------- const goods = { "red|32G": 3, "red|16G": 0, "blue|32G": 1, "blue|16G": 6 }, colorSelect = document.getElementById('colorSelect'), memorySelect = document.getElementById('memorySelect'), numberInput = document.getElementById('numberInput'), colorInfo = document.getElementById('colorInfo'), memoryInfo = document.getElementById('memoryInfo'), numberInfo = document.getElementById('numberInfo'), nextBtn = document.getElementById('nextBtn'), mediator = (function () { return { changed(obj) { const color = colorSelect.value, memory = memorySelect.value, number = numberInput.value, stock = goods[`${color}|${memory}`] if (obj === colorSelect) { colorInfo.innerHTML = color } else if (obj === memorySelect) { memoryInfo.innerHTML = memory } else if (obj === numberInput) { numberInfo.innerHTML = number } if (!color) { nextBtn.disabled = true nextBtn.innerHTML = '请选择手机颜色' return } if (!memory) { nextBtn.disabled = true nextBtn.innerHTML = '请选择内存大小' return } if (Number.isInteger(number - 0) && number < 1) { nextBtn.disabled = true nextBtn.innerHTML = '请输入正确的购买数量' return } nextBtn.disabled = false nextBtn.innerHTML = '放入购物车' } } })() colorSelect.onchange = function () { mediator.changed(this) } memorySelect.onchange = function () { mediator.changed(this) } numberInput.oninput = function () { mediator.changed(this) }
19.原型模式
- 原型模式是指原型实例指向创建对象的种类,通过拷贝这些原型来创建新的对象,说白了就是克隆自己,生成一个新的对象。
优点
- 不再依赖构造函数或者类创建对象,可以将这个对象作为一个模板生成更多的新对象。
缺点
- 对于包含引用类型值的属性来说,所有实例在默认的情况下都会取得相同的属性值。
例子
const user = { name:'小明', age:'30', getInfo(){ console.log(`姓名:${this.name},年龄:${this.age}`) } } const xiaozhang = Object.create(user) xiaozhang.name = '小张' xiaozhang.age = 18 xiaozhang.getInfo() // 姓名:小张,年龄:18 user.getInfo() // 姓名:小明,年龄:30
20.备忘录模式
- 备忘录模式就是在不破坏封装的前提下,捕获一个对象内部状态,并在该对象之外保存这个状态,以保证以后可以将对象恢复到原先的状态。
优点
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
- 实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点
- 如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
例子
// 棋子 class ChessPieces { constructor(){ this.chess = {} } // 获取棋子 getChess(){ return this.chess } } // 记录棋路 class Record { constructor(){ this.chessTallyBook = [] // 记录棋路 } recordTallyBook(chess){ // console.log(this.chessTallyBook.includes(chess)) const isLoadtion = this.chessTallyBook.some( item=>item.location === chess.location ) if(isLoadtion){ console.log(`${chess.type},${chess.location}已存在其他棋子`) }else { this.chessTallyBook.push(chess) } // this.chessTallyBook.some(item=>item.location === chess.location) } getTallyBook(){ return this.chessTallyBook.pop() } } // 下棋规则 class ChessRule { constructor(){ this.chessInfo = {} } playChess(chess){ this.chessInfo = chess } getChess(){ return this.chessInfo } // 记录棋路 recordTallyBook(){ return new ChessPieces(this.chessInfo) } // 悔棋 repentanceChess(chess){ this.chessInfo = chess.getTallyBook() } } const chessRule = new ChessRule() const record = new Record() chessRule.playChess({ type:'黑棋', location:'X10,Y10' }) record.recordTallyBook(chessRule.getChess())//记录棋路 chessRule.playChess({ type:'白棋', location:'X11,Y10' }) record.recordTallyBook(chessRule.getChess())//记录棋路 chessRule.playChess({ type:'黑棋', location:'X11,Y11' }) record.recordTallyBook(chessRule.getChess())//记录棋路 chessRule.playChess({ type:'白棋', location:'X12,Y10' }) console.log(chessRule.getChess())//{type:'白棋',location:'X12,Y10'} chessRule.repentanceChess(record) // 悔棋 console.log(chessRule.getChess())//{type:'黑棋',location:'X11,Y11'} chessRule.repentanceChess(record) // 悔棋 console.log(chessRule.getChess())//{type:'白棋',location:'X11,Y10'}
21.桥接模式
- 桥接模式是指将抽象部分与它的实现部分分离,使它们各自独立的变化,通过使用组合关系代替继承关系,降低抽象和实现两个可变维度的耦合度。
优点
- 抽象和实现的分离。优秀的扩展能力。实现细节对客户透明。
缺点
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
例子
- 比如我们所用的手机,苹果的iphoneX,和华为的mate40,品牌和型号就是它们共同的抽象部分,可以把他们单独提取出来。
class Phone { constructor(brand,modle){ this.brand = brand this.modle = modle } showPhone(){ return `手机的品牌:${this.brand.getBrand()},型号${this.modle.getModle()}` } } class Brand { constructor(brandName){ this.brandName = brandName } getBrand(){ return this.brandName } } class Modle { constructor(modleName){ this.modleName = modleName } getModle(){ return this.modleName } } const phone = new Phone(new Brand('华为'),new Modle('mate 40')) console.log(phone.showPhone())
22.访问者模式
- 访问者模式是将数据的操作和数据的结构进行分离,对数据中各元素的操作封装独立的类,使其在不改变数据结构情况下扩展新的数据。
优点
- 符合单一职责原则。具有优秀的扩展性和灵活性。
缺点
- 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
例子
class Phone { accept() { throw new Error('子类的accept必须被重写') } } class Mata40Pro extends Phone { accept() { const phoneVisitor = new PhoneVisitor() return phoneVisitor.visit(this) } } class IPhone13 extends Phone { accept() { const phoneVisitor = new PhoneVisitor() return phoneVisitor.visit(this) } } // 访问者类 class PhoneVisitor { visit(phone) { if (phone.constructor === IPhone13) { return { os: 'ios', chip: 'A15仿生芯片', screen: '电容屏' } } else if (phone.constructor === Mata40Pro) { return { os: 'HarmonyOS', chip: 'Kirin 9000', GPUType: 'Mali-G78', port: 'type-c' } } } } const mata40Pro = new Mata40Pro() console.log(mata40Pro.accept())
23.解释器模式
- 解释器模式提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口该接口,该接口解释一个特定的上下文。
优点
- 可扩展性比较好,灵活。增加了新的解释表达式的方式。
缺点
- 可利用场景比较少,在web开发中几乎不可见。对于复杂的环境比较难维护。
- 解释器模式会引起类膨胀。它还采用递归调用方法,没控制好可能会导致崩溃。
例子
class TerminalExpression { constructor(data) { this.data = data } interpret(context) { if (context.indexOf(this.data) > -1) { return true; } return false; } } class OrExpression { constructor(expr1, expr2) { this.expr1 = expr1; this.expr2 = expr2; } interpret(context) { return this.expr1.interpret(context) || this.expr2.interpret(context); } } class AndExpression { constructor(expr1, expr2) { this.expr1 = expr1; this.expr2 = expr2; } interpret(context) { return this.expr1.interpret(context) && this.expr2.interpret(context); } } class InterpreterPatternDemo { static getMaleExpression() { const robert = new TerminalExpression("小明"); const john = new TerminalExpression("小龙"); return new OrExpression(robert, john); } static getMarriedWomanExpression() { const julie = new TerminalExpression("张三"); const married = new TerminalExpression("小红"); return new AndExpression(julie, married); } static init(args) { const isMale = this.getMaleExpression(); const isMarriedWoman = this.getMarriedWomanExpression(); console.log(`小龙是男性?${isMale.interpret("小龙")}`) console.log(`小红是一个已婚妇女?${isMarriedWoman.interpret("小红 张三")}`) } } InterpreterPatternDemo.init()
总结
- 以上是我历时将近一个月的学习总结,然而一个月的时间是远远不够的,在写完这篇文章后,依旧对某些设计模式的应用场景缺乏了解。设计模式是要长时间深入研究的知识点,需要结合实际的场景去练习模仿,不断的去思考。另外由于js的特性,很多设计模式在js中是残缺的,非完全体的,强行用js去模仿传统的设计模式显的有点鸡肋。但是随着typescript的出现,设计模式在ts中可以无限接近传统的设计模式,后续计划写一篇ts版本的设计模式博客。
- 本片文章学习来源:
- 书籍《JavaScript设计模式与开发实践》
- 双越老师的:《Javascript 设计模式系统讲解与应用》
- 以及百度搜索借鉴的各种案例
加载全部内容