当前位置: 移动技术网 > IT编程>网页制作>HTML > 荐 适合前端学习的设计模式有哪些?

荐 适合前端学习的设计模式有哪些?

2020年07月17日  | 移动技术网IT编程  | 我要评论

1.单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点

1.场景

  • 登录弹框案例:
    • 点击登录按钮生成弹窗
    • 无论点击多少次,只生成一次

2.代理单例

  • 使用代理实现单例模式
    • 用户可以通过类来创建一个普通的实例
    • 也可以通过代理来设置这个实例只能创建一次
  • 优点
    • 提高了该类的复用性和可扩展性
  1. 首先创建一个构造函数(我们假设这个单例模式是用来创建div的)
    1. 定义一个init方法
const CreateDiv = function(html) {
	this.html = html
  this.init()
}
CreateDiv.prototype.init = function (){
  let div = document.createElement('div')
  div.innerHTML = this.html
  document.body.appendChild(div)
}
  1. 下面我们来创建一个普通的实例
var a = new CreateDiv('我是普通的div1')
var b = new CreateDiv('我是普通的div2')
// <div>我是普通的div1<div>
// <div>我是普通的div2<div>
  1. 下面我们引入代理,来实现单例模式
// 代理
let ProxySingletonCreateDiv = (function(){
  let instance
  return function(html) {
    if(!instance){
      // 如果不存在instance实例,则创建一个
      instance = new CreateDiv(html)
    }else {
      // 如果存在,则直接返回该实例
      return instance
    }
  }
})
// 创建实例
var a = new CreateDiv('我是普通的div1')
var b = new CreateDiv('我是普通的div2')
// (a === b)

代理单例模式的分层:

  1. 构造函数:
    1. 获取参数
    2. 执行初始化方法
  2. 初始化方法:
    1. 功能实现
  3. 代理:
    1. 对实例是否创建过进行判断
      1. 如果创建过,直接返回实例
      2. 如果没创建过,创建一个实例

3.惰性单例

  • 惰性单例是指在需要的时候才创建对象实例
    • 惰性单例是单例模式的重点,开发中使用频率高

实现

登录框案例:点击多次,只能创建一个登录框

  1. 创建一个只用来返回单例的方法
const getSingle = function (fn) {
  let result
  return function() {
    return result || (result = fn.apply(this,arguments))
  }
}
  1. 创建一个创造登录框的方法
const createLoginLayer = function () {
  let div = document.createElement('div')
  div.innerHTML = '我是登录框'
  div.style.display = 'block'
  document.body.appendChild('div')
  return div
}
  1. 使用
let createSingleLoginLayer = getSingle(createLoginLayer)

document.querySelect('#btn').onclick = function (){
  let loginLayer = createSingleLoginLayer()
  loginLayer.style.display = 'block'
}

惰性单例模式的分层:

  1. 返回单例的方法层
    1. 判断是否已有单例
      1. 使用短路运算
        1. 如果已有直接返回单例
        2. 如果没有创建新单例返回
  2. 功能方法层
    1. 只关注功能实现
    2. Do something
  3. 创建单例层
    1. 使用单例方法创建实例,参数为功能方法

2.策略模式

定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换

1.场景

  • 根据员工的绩效等级来发放年终奖:
    • 绩效S的年终奖为工资的4倍
    • 绩效A的年终奖为工资的3倍
    • 绩效B的年终奖为工资的2倍
  • 代码实现:
// 定义各个等级的计算函数,用一个总的对象包裹起来
const strategies = {
  'S':function(salary) {
    return salary * 4
  },
  'A':function(salary) {
    return salary * 3
  },
  'B':function(salary) {
    return salary * 2
  }
}

// 暴露接口
let calculateBonus = function(level,salary){
   return strategies[levels](salary)
}

// 计算等级S的工资
calculateBonus('S',20000) // 80000
// 计算等级A的工资
calculateBonus('A',10000) // 30000

2.策略模式实现缓动动画

  1. 定义Animate类,并初始化它
const Animate = function (dom) {
  this.dom = dom
  this.startTime = 0 // 动画开始时间
  this.startPos = 0 // dom初始位置
  this.endPos = 0 // dom结束位置
  this.propertyName = null // dom节点需要操控的属性名
  this.easing = null // 缓动算法
  this.duration = null // 动画持续时间
}
  1. 写出动画启动的方法
// 动画的启动方法
Animate.prototype.start = function (propertyName,endPos,duration,easing) {
  this.startTime = Date.now() // 动画启动的时间
  this.startPos = this.dom.getBoundingClientRect()[propertyName] // get方法是原生api,获取当前位置
  this.propertyName = propertyName // 操控的css属性名
  this.endPos = endPos // 目标位置
  this.duration = duration // 动画持续时间
  this.easing = tween[easing] // 缓动算法 tween方法未定义
  const self = this
  let timeId = setInterval(() => {
    if (self.step() === false) {
      // 如果动画已结束,则清楚定时器
      clearInterval(timeId)
    }
  }, 1000)
  }
  1. 写出每一帧需要做的事情
Animate.prototype.step = function () {
  let t = Date.now()
  if (t >= this.startTime + this.duration) {
    // 如果到了预定的时间(当前时间+每次运动的时间),更新css属性
    this.update(this.endPos)
    return false
  }
  let pos = this.easing(
    t - this.startTime, // 总共运行了多久
    this.startPos, // 开始位置
    this.endPos - this.startPos, // 运动的距离
    this.duration // 每帧的时间
  )
  this.update(pos)
}
  1. 动画节点的更新方法
Animate.prototype.update = function (pos) {
  this.dom.style[this.propertyName] = pos + 'px'
}
  1. 实例化并使用它
let div = document.querySelector('.ball')
let animate = new Animate(div)
animate.start('right', 500, 1000, 'strongEaseIn')

3.更广义的策略模式

实际开发中,通常会把使用策略模式来封装一系列的‘业务规则’,只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们

表单校验

4.策略模式优缺点

  • 优点:
    • 利用组合,委托,多态等技术和思想,有效避免多重条件选择语句
    • 对开放-封闭原则完美支持,将算法封装在独立的对象中,使得他们易于切换、理解、扩展
    • 策略模式中的算法也可以复用在系统的其他地方
  • 缺点:
    • 会在程序中增加许多策略类或策略对象
    • 要使用策略模式,必须了解制定好的每一条策略,才能选出最优最合适的策略。这就会把一个策略对象的细节向外界暴露出来,违反了隐藏原则

3.代理模式

定义:代理模式是为一个对象提供一个代用品,以便控制对它的访问

1.保护代理

  • 代理代替主体过滤掉一些不符合规则的请求,或处理一些主体不易处理的请求,这就叫保护代理
// 某明星
let star = {
  name: 'tongmian',
  age: '20',
  height: 175
}
// 经纪人
let proxy = new Proxy(star, {
  get: function (target, key) {
    if (key === 'height') {
      // 有人获取明星的身高时,谎报身高
      return target.height + 5
    }
    return target[key]
  }
})

console.log(proxy.height) // 180

2.虚拟代理

  • 对于一些比较耗性能的请求,代理选择在合适的时机(真正需要的时候)再向主体请求,这就叫虚拟代理
虚拟代理实现图片懒加载(不使用defineproperty和Proxy)
// 创建图片的方法
let myImage = (function(){
  let imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return function(src){
      imgNode.src = src
    }
})()
// 中间的代理
let proxyImage = (function(){
  let img = new Image
  img.onload = function(){
    myImage.setSrc(this.src)
  }
  return function(src){
      myImage.setSrc('占位用的图片地址')
      img.src = src
    }
})()

proxyImage('图片真正的地址')
虚拟代理合并HTTP请求
  • 假设有一个文件列表,选择每个文件时就会发送请求上传文件,那么这样是十分耗性能的
  • 解决的办法有两个:
    • 选择文件时储存文件ID ,在点击确定时同时进行上传(更改了需求)
    • 使用虚拟代理进行合并:
// 文件上传的方法(主体)
let updateFile = function(id) {
  console.log('开始上传文件')
}

// 代理
let proxyUpdateFile = (function(){
  let cache = []
  let timer
  return function(id){
    cache.push(id)
    if(timer){
      return
    }
    timer = setTimeout(function(){
      updateFile(cache.join(','))
      clearTimeout(timer)
      timer = null
      cache.length = 0
    },2000)
  }
})()


let checkbox = document.querySelect('.input')
checkbox.forEach(item=>{
  item.onclick = ()=>{
    if(this.checked === true) {
      proxyUpdateFile(this.id) // 执行代理传入ID
    }
  }
})
虚拟代理实现惰性加载
  • 需求:
    • 一个名为miniConsole的工具
    • 按F2时弹出div框,里面呈现用户使用miniConsole.log打印的内容
  • 梳理:
    • miniConsole.js不需要在一开始就引入,因为用户很可能不会去F2查看
    • 写入一个代理方法,对用户的miniConsole.log操作进行储存
      • 在用户按下F2的时候将miniConsole.js引入
let miniConsole = (function(){
  let cache = []
  let handler = function(e) {
    if(e.keyCode === 113) {
      let script = document.createElement('script')
      script.onload = function(){
        for(let i = 0,fn ; fn = cache[i++]) {
          fn()
        }
      }
      script.src = 'miniConsole.js'
        document.getElementByTagName('head')[0].appendChild(script)
        document.body.removeEventListener('keydown',handler) // 只加载一次
    }
  }
  document.body.addEventListener('keydown',handler,false)
    return {
      log:function(){
        let args = arguments
        cache.push(function(){
          return miniConsole.log.apply(miniConsole,args)
        })
      }
    }
})()
miniConsole.log(1)
// miniConsole.js代码
miniConsole = {
  log:function(){
    console.log(Array.prototype.join.call(arguments))
  }
}

3.缓存代理

  • 缓存代理可以为一些开销大得运算结果提供暂时得储存,在下次运算时,如果传递进来得参数跟之前一直,则直接返回前面储存得计算结果
斐波那契数列计算
  • 普通写法
let count = 0
let fbnqFn = n => {
count = count + 1
if (n === 1 || n === 2) {
return 1
}
return fbnqFn(n - 2) + fbnqFn(n - 1)
}

console.log(fbnqFn(41)) // 165580141
console.log(count+'次计算') // 331160281次计算
  • 代理写法
let count = 0 // 计数

// 本体 只用来计算斐波那契数列 只计算新式子
let fbnqFn = n => {
count = count + 1
if (n === 1 || n === 2) {
return 1
}
return proxyFbnq(n - 2) + proxyFbnq(n - 1) // 这里不再是调用自身
}

// 代理 保存已经计算的式子
let proxyFbnq = (() => {
let temp = {}
return function (n) {
if (n in temp) {
return temp[n]
}
return (temp[n] = fbnqFn(n)) // 新的式子丢回本体计算 并保存
}
})()

console.log(proxyFbnq(41)) // 165580141 
console.log(count + '次计算') // 41次计算
  • 小结:
    • 使用缓存代理处理一些需要大量计算的函数时,计算量大大减少了!

4.迭代器模式

定义:迭代器模式是指提供一种方法(顺序)访问一个对象中的各个元素,而又不需要暴露该对象的内部

小结:

  • 迭代器模式是一种相对简单的模式,简单到我都认为它不是一种设计模式,目前的绝大部分语言都内置了迭代器

5.发布-订阅模式(观察者)

定义:发布-订阅模式又叫观察者模式,它定义了对象间的一种一对多的依赖关系。

当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知

1.场景

  • 小明看中一个小区的房子,售楼中心告诉小明,过一段时间有活动,到时候通知他
    • 小明在售楼中心留下电话,同时留下电话的还有小红,小白
    • 过了一段时间活动开始,售楼中心根据他们留下的电话,依次给他们发送短信通知
    • 小明、小红、小白就是订阅者
    • 售楼中心就是发布者
    • 这就是发布-订阅模式
  • 小甲下载了一个app,第一次打开的时候app提示小甲:是否接受通知
    • 小甲选择了是,同样选择是的还有小乙,小丙,小丁
    • 过了几天app推出了一个满10减5的活动,将这个消息推送到了app上
    • 订阅了app消息的小甲等人的手机在第一时间弹出了这个活动的消息
    • 小甲等人就是订阅者
    • app就是发布者
    • 这就是发布-订阅模式

DOM事件就是最好的发布-订阅模式的实例

// 订阅者 订阅了点击事件,持续监听它
document.querySelect('#btn').onclick = function () {
  alert('我被点击了')
}

// 发布者
document.querySelect('#btn').click() // 模拟用户点击

2.通用的发布-订阅模式

// 拥有发布 和 订阅 功能的一个对象
let event = {
  clientList : {}, // 缓存列表,存放给订阅者发送的信息
  listen: (key,fn)=>{ // 设置key是保证订阅者只接收到它想要的消息
    if(!this.clientList[key]){
      // 如果这个key不存在则新建一个
      this.clientList[key] = []
    }
    this.clientList[key].push( fn ) // 将消息添加到缓存
  },
  trigger: ()=>{
    // 把参数的第一个取出来给key(关键字必须放在参数第一位)
    let key = Array.prototype.shift.call( arguments ) 
    let fns = this.clientList[ key ]
    if( !fns || fns.length === 0 ) { // 如果没有绑定的消息
      return false
    }
    for( let i = 0,fn; fn = fns[i++] ) {
      fn.apply(this,arguments)
    }
  }
}
// 一个拷贝的函数 实际开发中这个可以去掉,直接使用event来实现
let installEvent = (obj)=>{
  for(let i in event){
    obj[i] = event[i]
  }
}
  • 使用通用的发布-订阅模式来处理售楼事件
let saleOffices = {} // 创建一个发布-订阅对象

installEvent(saleOffices)

saleOffices.listen('小户型',(price)=>{console.log('价格' + price)})//小明的订阅
saleOffices.listen('大户型',(price)=>{console.log('价格' + price)})//小明的订阅

// 售楼中心开始发布消息
saleOffices.trigger('小户型',10000)
saleOffices.trigger('大户型',12000)

小明如果突然不想买房,那么他就不愿意再接受售楼处发送的短信了,这时候应该取消订阅

  • 给event添加一个取消订阅的方法
event.remove = (key,fn) {
  let fns = this.clientList[key]
  if( !fns ){ // 如果对应的key没有被人订阅,那么直接返回
    return false
  }
  if( !fn ){ // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
    fns && (fns.length = 0)
  }else {
    for(let i = fns.length -1 ; i >= 0 ; i--){
      let _fn = fns[i]
      if(_fn === fn){
        fns.splice(i,1)
      }
    }
  }
}

6.适配器模式

犹如插头转换器一样,适配器的作用就是将插座的插孔转换成手机充电器可用的插孔

// 插座类
class Power{
  constructor(){
    this.serveVoltage = 110 // 电压为110伏
    this.serveShape = 'triangle' // 插头形状为三角形
  }
}

// 适配器
class Adaptor{
  constructor(){
    // 插向插座的一面
    this.consumeVoltage = 110
    this.consumeShape = 'triangle'
  }
  // 面向手机充电器的一面
  userPower(power){
    if(!power){
      throw new Error('请接入电源')
    }
    if(power.serveVoltage !== this.consumeVoltage || power.serveShage !== this.consumeShape){
      throw new Error('电源规格不对')
    }
    // 修改面向手机充电器一面的接口参数
    this.serveVoltage = 220
    this.serveShage = 'double'
  }
}

// 手机充电器
class User {
  constructor(){
    this.serveVlotage = 220
    this.serveShage = 'double'
  }
  userPower(power){
    if(!power){
      throw new Error('请接入电源')
    }
    if(power.serveVoltage !== this.consumeVoltage || power.serveShage !== this.consumeShape){
      throw new Error('电源规格不对')
    }
  }
}

// 使用
let power = new Power()
let user = new User()
let adaptor = new Adaptor()
adaptor.usePower(power)
user.usePower(adaptor)

使用场景

  1. 开发中需要对接口的提供者和消费者进行兼容时
  2. 对旧老接口进行改造升级,但是无法一次性改造完成时

7.装饰器模式

定义

向一个现有的对象添加新的功能,同时又不改变其结构。

使代码更加的优雅

手机壳就像是装饰器模式,不会改变手机原有的功能,但是增加了防摔,防窥,防水等效果

ES5下的装饰器

// 手机
function Phone(){}
Phone.prototype.mackcall = function(){
  console.log('打电话')
}

// 装饰器
function decorate(target){
  target.prototype.shell = function(){
    console.log('手机壳')
  }
  return target
}

// 使用
Phone = decorate(Phone)
let phone = new Phone()

ES7装饰器语法

目前ES7已加入装饰器语法,但nodejs和浏览器都尚未支持,如果想使用,需要通过babel进行转译

  • 需要安装babel以及另一个babel插件
  1. 用装饰器装饰类
// 装饰器
function writeCode(){
	console.log('写代码')
}

@writeCode // 可以写多个装饰器
class Phone{
  // 手机
}

var phone = new Phone()
phone.writeCode() // 写代码
  1. 用装饰器装饰函数
function log(target,name,descriptor){
  // target此时为Math.prototype
  // name此时为方法名 add
  // descriptor:
  	// value:函数本身
  	// enumerable:是否可遍历
  	// configurable:是否可配置
  	// writale:是否可重写
}

class Math{
  @log
  add(a,b){
    return a+b
  }
}

装饰器工具

  • core-decoratorsnpm包将常用的装饰器工具进行了封装
使用
import {readonly,autobind,deprecate} from 'core-decorators'
// import为ES6语法,需要用webpack编译后引入
// readonly:只读
// autobind:自动绑定,把this强制绑定到实例上
// deprecate:弃用 警告你的用户这个方法即将弃用

8.区分装饰、适配、代理

  • 适配器模式:提供不同的新接口,用作接口转换、处理兼容
  • 代理模式:提供一摸一样的新接口,对行为进行拦截
  • 装饰器模式:直接访问原接口,对原接口进行功能上的增强

9.外观模式

定义

把一堆复杂的接口逻辑放在一起(函数),对外提供一个更高级的统一接口,使外部对于子接口的访问和控制更加容易

使用场景

  • DOM事件监听有兼容问题,使用外观模式对其进行处理
function addEvent(dom,type,fn){
  if(dom.addEventListener){
    // 支持addEventListener方法的浏览器
    dom.addEventListener(type,fn,false)
  }else if(dom.attachEvent){
    // IE浏览器
    dom.attachEvent('on'+type,fn)
  }else {
    // 都不支持
    dom['on'+type] = fn
  }
}
// 不需要考虑兼容性的问题,直接使用即可
addEvent(myDom,'click',function(){ Do Something })

10-状态模式

通常的封装,一般都是封装对象的行为,

在状态模式中是把对象的每种状态都进行独立封装

1.场景

  • 以下情况中,我们应该考虑使用状态模式
  1. 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
  2. 一个操作含有大量的分支语句,而且这些分支语句依赖于该对象的状态
  • 比如:
    • 游戏中一个人物的技能,有冷却、禁用、可用等状态,每次只可能存在一种状态,而且必定是从其中一种状态转换成另一种状态
  • 再比如:
    • 音乐播放器的播放模式有,单曲循环模式、顺序播放模式、列表模式、随机模式,且每次只会存在一种状态,且必定是从其中一种状态切换成另一种状态

2.案例

  • 一盏台灯有关闭,弱光,强光、三种状态
class Lamp{
  constructor(){
    this.offLightState = FSM.offLightState()
  }
  pressButton(){
    this.state.trigger.call(this)
  }
}
// FSM 有限状态机的缩写
// FSM定义:
	// 1.状态总数是有限的
	// 2.任一时刻,只处在一种状态之中
	// 3.某种条件下,会从一种状态转变到另一种状态
const FSM = {
  offLightState:{
    // 关闭状态下触发trigger
    trigger(){
      console.log('弱光')
      this.state = FMS.weakLightState
    }
  },
  weakLightState:{
    // 弱光状态下触发trigger
    trigger(){
      console.log('强光')
      this.state = FMS.strongLightState
    }
  },
  strongLightState:{
    // 强光状态下触发trigger
    trigger(){
      console.log('关闭')
      this.state = FMS.offLightState
    }
  }
}

其它模式等有人看的时候再补充吧

本文地址:https://blog.csdn.net/weixin_45550048/article/details/107381861

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网