一文入门设计模式---从前端角度简谈设计模式

156 阅读14分钟

一、概述

首先,什么是设计模式?我们可以简单理解为一些从大量的经验中总结出来的套路,使用这些套路可以帮我们有效解决问题,使代码变得更加优雅。本文将从前端的角度,系统介绍各设计模式,并通过一些例子以便大家理解。设计模式可能在平时业务中很少用到,但对于比较复杂的项目,或者想要自己开发一些工具或组件的时候,还是非常实用的,你可以在平时开发中根据自己的业务择机使用。就算用不到,学习设计模式也可以让你具备以更高层次看待代码的能力。本文是学习《kerwin的设计模式》后,总结而写,各位看官请耐心看完或者先收藏,慢慢阅读。大家有兴趣可以在b站一键三连,支持优秀的老师,支持免费的网课,然后免费学习视频。

其次,本文需要良好的JS基础,如需要熟悉原型、类、迭代器、链表结构等。希望你在阅读本文前已经熟悉这些知识。如果没有这些前置知识,你依然可以阅读这篇文章,当遇到难以理解的知识点时可以单独对该知识点内容进行学习,或者暂时跳过。

二、走进设计模式

2.1 工厂模式

工厂模式,顾名思义,可以批量生产对象,对于一些拥有共同属性或方法的对象,我们可以封装一个工厂,工厂中包含通有的属性和方法,通过传入不同参数,来制造不同的对象。

// 举例:后台管理系统,登录不同账号,展示不同页面

// 工厂类,构造函数中包含角色和页面,类的静态方法中包含根据角色返回页面的工厂函数
class User {
  constructor(role, pages) {
    this.role = role
    this.pages = pages
  }

  static UserFactory(role) {
    switch (role) {
      case 'superadmin':
        return new User('superadmin', ['home', 'user-manage', 'right-manage', 'news-manage'])
      case 'admin':
        return new User('admin', ['home', 'user-manage', 'news-manage'])
      case 'editor':
        return new User('editor', ['home', 'news-manage'])
      default:
        throw new Error('参数错误')
    }
  }
}
// 直接调用静态方法即可即可生成目标对象
const superadmin = User.UserFactory('superadmin')
const admin = User.UserFactory('admin')

2.2 抽象工厂模式

抽象工厂模式基于工厂模式改造,不直接生产对象,而是生产类。先有一个父类,包含公有的属性和方法,但是公有的方法并没有函数体,函数体需要在子类中实现。有多个子类继承父类,并拥有自己独特的属性和方法

// 以2.1的例子继续改造

// 父类
class User {
  constructor(name, role, pages) {
    this.name = name
    this.role = role
    this.pages = pages
  }

  welcome() {
    console.log('欢迎回来', this.name)
  }

  dataShow() {
    // 抽象类,需在子类实现,js暂无此关键字,可参考ts用法
    throw new Error('js中抽象方法需要在子类实现')
  }
}

// 每一个子类都继承自父类并且可以扩展自己的属性和方法
class SuperAdmin extends User {
  constructor(name) {
    super(name, 'superadmin', ['home', 'user-manage', 'right-manage', 'news-manage'])
  }
  dataShow() {
    console.log('super-admin---datashow')
  }
  addRight() {}
  addUser() {}
}

class Admin extends User {
  constructor(name) {
    super(name, 'admin', ['home', 'user-manage', 'news-manage'])
  }

  dataShow() {
    console.log('admin---datashow')
  }
  addUser() {}
}

class Editor extends User {
  constructor(name) {
    super(name, 'editor', ['home', 'news-manage'])
  }

  dataShow() {
    console.log('editor---datashow')
  }
}

// 这是抽象工厂,只返回类
function getUserClassFactory(role) {
  switch (role) {
    case 'superadmin':
      return SuperAdmin
    case 'admin':
      return Admin
    case 'editor':
      return Editor
    default:
      throw new Error('参数错误')
  }
}
// 根据需求拿到所需类后再实例化使用
const currentClass = getUserClassFactory('superadmin')
const currentRole = new currentClass('zjm')
currentRole.dataShow()

2.3 建造者模式

建造者模式关注过程和细节,将复杂对象的构建层和表示层相互分离,如果两个类具有同样的执行逻辑,那么此时可以新建一个类,在该类中统一执行逻辑

// 举例:此时有Navbar和List两个类,两个类都有init,getData,render方法,那么我们就可以创建一个新类来执行逻辑,而Navbar和List两个类只需关心自己内部的使用逻辑。

class Navabar {
  init() {
    console.log('navabar-init')
  }
  getData() {
    console.log('navabar-getdata')
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('navabar-111')
      }, 1000)
    })
  }
  render() {
    console.log('navabar-render')
  }
}

class List {
  init() {
    console.log('list-init')
  }
  getData() {
    console.log('list-getdata')
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('list-111')
      }, 1000)
    })
  }
  render() {
    console.log('list-render')
  }
}

//内部创建一个函数,用于统一执行逻辑
class Creater{
  async startBuild(builder) {
    builder.init()
    await builder.getData()
    builder.render()
  }
}

const operator = new Creater()
operator.startBuild(new Navabar())
operator.startBuild(new List())

2.4 单例模式

单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点,主要解决一个全局使用的类频繁地销毁和创建,占用内存

// 以登录过期时,跳出对话框为例,该对话框在过期时才创建,期间对话框并不销毁,此处ES5实现还利用了闭包

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<style>
  .modal {
    position: fixed;
    width: 200px;
    height: 200px;
    line-height: 200px;
    text-align: center;
    background-color: pink;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
</style>
<body>
  <button class="open">打开</button>
  <button class="close">关闭</button>
  <script>
    
    // ES5实现,通过闭包保存了instance,以保证每次调用返回的都是最初创建的那个实例
    // const Single = (function() {
    //   let instance = null
    //   return function() {
    //     if (!instance) {
    //       instance = document.createElement('div')
    //       instance.innerHTML = '登录对话框'
    //       instance.className = 'modal'
    //       document.body.appendChild(instance)
    //     }
    //     return instance
    //   }
    // })()


    // ES6实现
    class Single {
      // 注意,这里要使用静态属性和方法,这样instance和方法是属于类的
      // 如果不加Single,那instance是属于实例的,每次就会创建多个实例
      static instance = null
      
      static getInstance() {
        if(!Single.instance) {
          Single.instance = document.createElement('div')
          Single.instance.innerHTML = '登录对话框'
          Single.instance.className = 'modal'
          document.body.appendChild(Single.instance)
        }
        return Single.instance
      }
    }
    document.querySelector('.open').onclick = function() {
      const modal = Single.getInstance()
      modal.style.display = 'block'
    }
    document.querySelector('.close').onclick = function() {
      const modal = Single.getInstance()
      modal.style.display = 'none'
    }
  </script>
</body>
</html>

2.5 装饰器模式

装饰器模式可以很好的对已有功能进行扩展,不会更改原有代码

/* 
   1.这里举一个应用场景,点击某个选项时,有时具有附加功能,需要先给后台发送统计数据,以对展示内容做出调整;之后才正式执行业务逻辑;
   2.因此这里实现一个前置函数,在执行业务逻辑前执行
*/
Function.prototype.before = function(beforeFn) {
  const _this = this
  return function() {
    // 执行前置函数
    beforeFn.apply(this, arguments)
    // 执行原函数并返回原返回值
    return _this.apply(this, arguments)
  }
}

// 向后台发送统计数据(前置函数)
function log() {
  console.log('发送pv等统计数据')
}

// 原有的业务逻辑
function render() {
  console.log('执行业务逻辑,如跳转页面等')
}

// 一般log函数是异步的,因此在执行完log函数后在执行render函数,以往我们可能会将render函数的调用放在log函数内部,这样会导致高耦合,且违反了函数的单一功能原则,不推荐

// 如有需要时可以重写render函数,以执行前置函数
render = render.before(log)

// 利用定时器模拟按钮点击时,执行的操作
setTimeout(() => {
  render()
}, 1000)

2.6 适配器模式

将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作

// 举例,点击某一个按钮需要渲染百度地图,点击另一个按钮需要渲染高德地图
class GDMap {
  show() {
    console.log('渲染高德地图--粘贴的一堆代码')
  }
}

class BDMap {
  dispaly() {
    console.log('渲染百度地图--粘贴的一堆代码')
  }
}

// 创建适配器,以使不同的地图都可以调用同一个方法
class BDMapAdapter extends BDMap{
  constructor() {
    super()
  }
  show() {
    this.dispaly()
  }
}

// 该方法不管是要渲染百度地图或是高德地图都可以使用
function renderMap(map) {
  map.show()
}

renderMap(new GDMap())
renderMap(new BDMapAdapter())

2.7 策略模式

策略模式将算法(这里算法可以是一些简单的逻辑)的实现和使用分离开来,避免大量使用if和else进行判断,提高代码可读性。

 // 这里以一个列表为例,列表每一项左侧是新闻名字,右侧是新闻状态
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    li {
      display: flex;
      justify-content: space-around;
    }
    .green-text {
      color: green;
    }
    .yellow-text {
      color: yellow;
    }
    .red-text {
      color: red;
    }
  </style>
</head>
<body>
  <ul class="cont"></ul>

  <script>
    const list = [
      { title: '新闻1', type: 1 },
      { title: '新闻2', type: 1 },
      { title: '新闻3', type: 2 },
      { title: '新闻4', type: 3 },
    ]

    // 直接将算法(这里仅是一些简单的属性)提炼出来
    const listMapStrategy = {
      1: {
        text: '审核中',
        className: 'green-text'
      },
      2: {
        text: '待审核',
        className: 'yellow-text'
      },
      3: {
        text: '已驳回',
        className: 'red-text'
      }
    }

    const cont = document.querySelector('.cont')
    cont.innerHTML = list.map(item => {
      return `
        <li>
          <span>${item.title}</span>  
          <span class='${listMapStrategy[item.type].className}'>${listMapStrategy[item.type].text}</span>  
        </li>
      `
    }).join('')
  </script>
</body>
</html>

2.8 代理模式

代理模式为其他对象提供一种代理,以控制对这个对象的访问,相当于原对象把控制权交出去了

// ES6的proxy专为代理而生,可以执行非常多的操作,如对proxy不了解的,可以去单独学习
// 简单来说,通过new Proxy创建一个实例,当访问这个代理对象时,会调用其中的get方法;修改代理对象的值时,会调用其中的set方法

let obj = {
  name: 'zjm',
  age: 18
}

let proxyObj = new Proxy(obj, {
  get(target, key) {
    return target[key]
  },
  set(target, key, value) {
    target[key] = value
  }
})

proxyObj.age = 17
console.log(proxyObj.name)

2.9 观察者模式

观察者模式包含观察目标和观察者,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。观察者模式的缺点在于无法对事件的细分管控,只有一个notify方法,无法控制如'aaa'事件触发还是'bbb'事件触发。

注意:这里针对观察者模式和下一个模式-发布订阅者模式,不做过多讲解,感兴趣的朋友可以移步我之前的文章《js手写一:实现发布订阅者模式》。

    // 模拟点击菜单,右边和头部展示面包屑的功能(注意,实际开发时,左右组件是互不关联的)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      display: flex;
      height: 800px;
    }
    .box .left {
      width: 500px;
      height: 100%;
      background-color:pink;
    }
    .box .left li {
      cursor: pointer;
    }
    .box .right {
      flex: 1;
      background-color: yellow;
    }
  </style>
</head>
<body>
  <header class="header">路径</header>
  <div class="box">
    <div class="left">
      <ul>
        <li>首页</li>
        <li>用户管理</li>
        <li>新闻管理</li>
        <li>权限管理</li>
      </ul>
    </div>
    <div class="right">
      <div class="bread">ddd</div>
    </div>
  </div>

  <script>
    // 目标
    class SubJect {
      constructor() {
        this.observers = []
      }
      // 添加观察者
      add(observer) {
        this.observers.push(observer)
      }
      // 移除观察者
      remove(observer) {
        this.observers = this.observers.filter(item => item !== observer)
      }
      // 通知观察者执行
      notify(data) {
        this.observers.forEach(item => item.update(data))
      }
    }
    // 观察者
    class Observer {
      constructor(name) {
        this.ele = document.querySelector(name)
      }
      // 观察者数据更新
      update(data) {
        this.ele.innerHTML = data
      }
    }
    // 创建目标和观察者并添加观察者
    const subject = new SubJect()
    const observer1 = new Observer('.bread')
    const observer2 = new Observer('.header')
    subject.add(observer1)
    subject.add(observer2)
    // 测试代码:点击列表时,面包屑和头部数据更新为点击的数据项
    const lis = document.querySelectorAll('li') 
    lis.forEach(li => {
      li.addEventListener('click', () => {
        subject.notify(li.innerHTML)
      })
    })
  </script>
</body>
</html>

2.10 发布订阅者模式

需要通信时,首先需要主动订阅,和观察者模式的区别在于增加了订阅的事件类型。发布者和订阅者互不相关,对于他们来说,只需关注自己发出/接收数据就行,其他不用关心。

这个模式大家应该都很熟悉,Vue中的事件总线就是使用的发布订阅者模式,大家可以参照平时对事件总线的使用来更好的理解这个设计模式。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      display: flex;
      height: 800px;
    }
    .box .left {
      width: 500px;
      height: 100%;
      background-color:pink;
    }
    .box .left li {
      cursor: pointer;
    }
    .box .right {
      flex: 1;
      background-color: yellow;
    }
  </style>
</head>
<body>

  <header class="header">路径</header>
  <div class="box">
    <div class="left">
      <ul>
        <li>首页</li>
        <li>用户管理</li>
        <li>新闻管理</li>
        <li>权限管理</li>
      </ul>
    </div>
    <div class="right">
      <div class="bread">ddd</div>
    </div>
  </div>

  <script>
    // 同2.9观察者模式的例子
    class PubSub {
      constructor() {
        this.message = {}
      }
      // 增加了订阅的细分类型
      subscribe(type, fn) {
        if (!this.message[type]) {
          this.message[type] = []
        }
        this.message[type].push(fn)
      }
      // 发布事件
      publish(type, params) {
        if(!this.message[type]) return
        this.message[type].forEach(item => item(params))
      }
      // 解除订阅
      unsubscribe(type = null) {
        if (!type) {
          this.message = {}
        } else {
          this.message[type] = []
        }
      }
    }

    const pubsub = new PubSub()
    // 使用时,需先订阅,再发布,订阅者接到发布的通知时,则会依次调用
    pubsub.subscribe('bread',(data) => {
      const targetNode = document.querySelector('.bread')
      targetNode.innerHTML = data
    })
    pubsub.subscribe('header',(data) => {
      const targetNode = document.querySelector('.header')
      targetNode.innerHTML = data + '--header'
    })

    const lis = document.querySelectorAll('li')
    lis.forEach(li => {
      li.addEventListener('click', () => {
        pubsub.publish('bread', li.innerHTML)
        pubsub.publish('header', li.innerHTML)
      })
    })

    setTimeout(() => {
      // pubsub.unsubscribe('header')
      pubsub.unsubscribe()
    }, 3000);

  </script>
</body>
</html>

2.11 模块模式

模块模式对前端来说就是模块化开发,ES6Module通过 export 导出模块, 通过import导入模块,各模块根据导入导出关系建立联系。目前还在使用的还有CommonJs模块,这些是比较基础的前端知识,这里仅一带而过。

2.12 桥接模式

将抽象部分与他的实现部分分离,使他们都可以独立地变化,使用场景:一个类存在两个或多个独立变化的维度,且这两个维度都需要进行扩展

优点:有助于独立管理各组成部分

缺点:每使用一个桥接元素都要增加一次函数调用,有可能会提高系统的复杂程度。

这里以弹窗的动画效果和弹窗的形式举例(toast, dialog这些)。UI框架中,有很多诸如弹窗或者提示框这些类型,也有很多弹出的动画效果。弹窗效果可以内部扩展,弹窗形式也可以根据需求定义多个类,看完代码就很好理解了。

// 动画类(这里仅用个对象表示了),内部可以扩展多个动画,且实现了具体方法
const Animations = {
  debounce: {
    show(ele) {
      console.log(ele, '弹跳显示')
    },
    hide(ele) {
      console.log(ele, '弹跳隐藏')
    }
  },
  slide: {
    show(ele) {
      console.log(ele, '滑动显示')
    },
    hide(ele) {
      console.log(ele, '滑动隐藏')
    }
  },
  rotate: {
    show(ele) {
      console.log(ele, '旋转显示')
    },
    hide(ele) {
      console.log(ele, '旋转隐藏')
    }
  }
}
// 对话框,原型上有show方法,但是没有作具体实现,调用传入元素的show方法
function Toast(ele, animation) {
  this.ele = ele
  this.animation = animation
}

Toast.prototype.show = function() {
  // 抽象,即不具体实现show方法
  this.animation.show(this.ele)
}

Toast.prototype.hide = function() {
  this.animation.hide(this.ele)
}
// 模态框
function Modal(ele, animation) {
  this.ele = ele
  this.animation = animation
}

Modal.prototype.show = function() {
  // 抽象,即不具体实现show方法
  this.animation.show(this.ele)
}

Modal.prototype.hide = function() {
  this.animation.hide(this.ele)
}
// 弹窗类型和弹窗动画可以自由组合
const toast1 = new Toast('div1', Animations.debounce)
const rotate1 = new Modal('div2', Animations.rotate)

toast1.show()
rotate1.show()
setTimeout(() => {
  rotate1.hide()
}, 1000)

2.13 组合模式

组合模式在对象间形成树形结构,无需关心对象有多少层,调用时只需在根部进行调用,因此会使用到迭代。下面以两个例子来帮助理解

2.13.1 扫描文件夹

/*
遍历文件夹,文件夹下存在文件,结构如下
root
    html
        html1
        html2
    css
        css
    js
        js
*/

// 这里用js模拟,为简单起见不使用node模块,因此定义add方法手动添加
// 文件夹类
class Folder {
  constructor(folder) {
    this.folder = folder
    // 收集子文件夹
    this.list = []
  }
  add(folder) {
    this.list.push(folder)
  }
  scan() {
    console.log('开始扫描文件夹', this.folder)
    for(const item of this.list) {
    // 递归调用子目录下的scan方法,因此文件类中必须也实现scan方法
      item.scan()
    }
  }
}
// 文件类
class File {
  constructor(file) {
    this.file = file
    this.list = []
  }
  scan() {
    console.log('开始扫描文件', this.file)
  }
}
// 添加文件
const rootFolder = new Folder('root')

const htmlFolder = new Folder('html')
const jsFolder = new Folder('js')
const cssFolder = new Folder('css')

rootFolder.add(htmlFolder)
rootFolder.add(jsFolder)
rootFolder.add(cssFolder)

const htmlFile1 = new File('html1')
const htmlFile2 = new File('html2')
const cssFile = new File('cssFile')
const jsFile = new File('jsFile')

htmlFolder.add(htmlFile1)
htmlFolder.add(htmlFile2)
cssFolder.add(cssFile)
jsFolder.add(jsFile)

// 只需调用根节点的方法,就会依据树结构递归调用其余节点
rootFolder.scan()

// 开始扫描文件夹 root
// 开始扫描文件夹 html
// 开始扫描文件 html1
// 开始扫描文件 html2
// 开始扫描文件夹 js
// 开始扫描文件 jsFile
// 开始扫描文件夹 css
// 开始扫描文件 cssFile

2.13.2 动态渲染菜单侧边栏

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="root">
  // 目标是实现这种结构的侧边栏,其实结构就是2.13.1注释中的文件夹结构,因此,根据2.13.1改造就行,将原本的打印变为一些dom操作
    <!-- <ul>
      <li>用户管理
        <ul>
          <li>新建用户</li>
          <li>编辑用户</li>
        </ul>
      </li>
      <li>权限管理</li>
      <li>新闻管理</li>
    </ul> -->
  </div>
  <script>

    class Folder {
      constructor(folder) {
        this.folder = folder
        this.list = []
      }
      add(folder) {
        this.list.push(folder)
      }
      scan() {
        let childUl = null
        if (this.folder === 'root') {
          console.log('不创建节点')
        } else {
          const ulEle = document.createElement('ul')
          const liEle = document.createElement('li')
          liEle.innerHTML = this.folder
          // 这里有个相对难的就是第一次扫描时,需要在每一个li下创建一个ul并传递给下一级,以作为下一级目录的父节点
          childUl = document.createElement('ul')
          liEle.appendChild(childUl)
          ulEle.appendChild(liEle)
          root.appendChild(ulEle)
        }
        for(const item of this.list) {
          item.scan(childUl)
        }
      }
    }

    class File {
      constructor(file) {
        this.file = file
        this.list = []
      }
      scan(ele) {
        const liEle = document.createElement('li')
        liEle.innerHTML = this.file
        ele.appendChild(liEle)
      }
    }

    const rootFolder = new Folder('root')

    const htmlFolder = new Folder('用户管理')
    const jsFolder = new Folder('权限管理')
    const cssFolder = new Folder('新闻管理')

    rootFolder.add(htmlFolder)
    rootFolder.add(jsFolder)
    rootFolder.add(cssFolder)

    const htmlFile1 = new File('新增用户')
    const htmlFile2 = new File('查询用户')
    const cssFile = new File('新增权限')
    const jsFile = new File('新增新闻')

    htmlFolder.add(htmlFile1)
    htmlFolder.add(htmlFile2)
    cssFolder.add(cssFile)
    jsFolder.add(jsFile)

    rootFolder.scan()
  </script>
</body>
</html>

2.14 命令者模式

命令者模式由三种角色构成:发布者(invoker),接收者(receiver),命令对象(command)。发布者和接收者通过命令对象建立连接,彼此之间不需要知道对方的具体信息。这个模式的实现思路有点意思,可以为我们其他类似问题的处理提供思路。

Q:实现思路,如何建立联系?

A:通过参数传递,如发布者构造函数接收一个command,而command中实现了一个execute方法,而receiver中也实现了一个execute方法。因此,一调用发布者的execute方法,就会执行comand中的execute,已调用command中的execute方法,就会调用receiver的execute方法

class Invoker {
  constructor(command) {
    this.command = command
  }
  execute() {
    console.log('发布者执行了')
    this.command.execute()
  }
}
class Command {
  constructor(receiver) {
    this.receiver = receiver
  }
  execute() {
    console.log('命令对象-接收者如何执行')
    this.receiver.execute()
  }
}

class Receiver {
  execute() {
    console.log('接收者执行了')
  }
}

const receiver = new Receiver()
const command = new Command(receiver)
const invoker = new Invoker(command)
// 发布者执行,间接通过命令对象调用执行者
invoker.execute()

2.15 宏命令模式

命令模式业务场景可能比较少用,命令模式和组合模式结合就形成了组合命令模式,即宏命令模式。

举例:页面加载后,页面中的轮播图,瀑布流,选项卡等一起开始调用。

class MircoCommand {
  constructor() {
    // 初始化list用于保存所有command
    this.list = []
  }
  add(command) {
    this.list.push(command)
  }
  execute() {
    // 遍历list,调用每一个command中的execute,因此需要保证每一个command中有一个execute方法
    for(const item of this.list) {
      item.execute()
    }
  }
}

const Tabs = {
  execute() {
    console.log('tabs开始调用')
    // 可以单独对每一个功能模块进行扩展
    this.init()
    this.getData()
  },
  init() {
    console.log('tabs-init')
  },
  getData() {
    console.log('tabs-getData')
  }
}

const Swipper = {
  execute() {
    console.log('swipper开始调用')
  },
}

const mircoCommand = new MircoCommand()
mircoCommand.add(Tabs)
mircoCommand.add(Swipper)

// 唯一的执行者,发布命令后,每个子系统开始执行
mircoCommand.execute()

2.16 迭代器模式

迭代器模式不关心对象的内部构造,也可以按顺序访问其中的每个元素。迭代器是ES6的一个语法,他不是什么神秘的东西,这里不对其进行详细描述,可以理解为迭代器就是在内部实现了一个[Symbol.iterator]方法,而这个方法一定实现了一个next函数,通过调用next函数来逐步执行。部署了迭代器的对象可以通过for of遍历,如数组,字符串,arguments等,这里举三个例子来帮助理解迭代器模式

1.一个普通的数组,不通过for of遍历,通过next逐步调用,查看结果

2.一个类似于数组的对象,手动部署一个迭代器以使其可以被for of遍历

3.一个普通对象,使其可以被for of遍历

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // 
    const list = ['111', '222', '333']
    // 1.在浏览器打印可看到该数组的原型身上具有一个Symbol.iterator方法
    console.log(list, list[Symbol.iterator]())
    // 因此,可迭代对象除了直接遍历外,仍然可以通过这种方法遍历调用(for of 本质就是根据lenth属性调用next方法)
    let iterator = list[Symbol.iterator]()
    console.log(iterator.next())
    console.log(iterator.next())
    console.log(iterator.next())
    console.log(iterator.next())

    // 2.给类似于数组的对象部署一个迭代器,那么我们也可以通过for of遍历
    let obj = {
      0: 'zjm',
      1: '18',
      length: 2,
      [Symbol.iterator]: Array.prototype[Symbol.iterator]
    }

    for(const item of obj) {
      console.log(item)
    }

    // 3.给普通的对象部署一个迭代器
    // 目的是让这个对象可以进行for of遍历,并且拿到的是其中某一个数组的每一项
    
    const obj2 = {
      code: 200,
      msg: 'success',
      data: ['111', '222', '333'],
      [Symbol.iterator]: function() {
        let index = 0
        return {
          next:() => {
            if (index < this.data.length) {
              return {
                value: this.data[index++],
                done: false
              }
            } else {
              return {
                value: undefined,
                done: true
              }
            }
          }
        }
      }
    }

    // for (const item of obj2) {
    //   console.log(item)
    // }
    const it = obj2[Symbol.iterator]()
    console.log(it.next())
    console.log(it.next())
    console.log(it.next())
    console.log(it.next())
  </script>
</body>
</html>

2.17 职责链模式

职责链模式使多个对象都有机会处理请求,从而避免了请求的发送者与多个接收者直接的耦合关系。将这些接收者连接成一条链,顺着这条链传递该请求,直到找到能处理该请求的对象。

优点:符合单一职责,易于扩展。

这里以表单校验为例来说明职责链模式:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <input id="input" type="text">
  <button id="btn">注册</button>
  <script>
    // 普通写法,需要加入大量判断,耦合性强,
    // btn.addEventListener('click', () => {
    //   const value = input.value
    //   if (!value) {
    //     console.log('请输入内容')
    //   } else if (isNaN(value)) {
    //     console.log('请输入数字')
    //   } else if (value.length < 6) {
    //     console.log('长度需大于5位')
    //   } else {
    //     console.log('通过校验')
    //   }
    // })

    // 职责链模式

    function checkEmpty() {
      if (!input.value) {
        console.log('请输入内容')
        return
      }
      return 'next'
    }
    function checkIsNumber() {
      if (isNaN(input.value)) {
        console.log('请输入数字')
        return
      }
      return 'next'
    }
    function checkLength(value) {
      if (input.value.length < 6) {
        console.log('长度需大于5位')
        return
      }
      return 'next'
    }
    /* 
      这里可能有个地方大家觉得比较难理解,如第二次调用addRule方法,用代码推演下就容易理解了
      constructor(nextRule) {
        this.checkRule = nextRule
        this.nextRule = null
      }
      nextRule传入后经过实例化,得到的this.nextRule是一个实例对象,和第一次的nextRule是没关系的,即链表中节点的传递  
    */
    
    class CheckChain {
      constructor(checkRule) {
        this.checkRule = checkRule
        this.nextRule = null
      }
      check() {
        this.checkRule() === 'next' ? this.nextRule.check() : null
      }
      addRule(nextRule) {
        this.nextRule = new CheckChain(nextRule)
        return this.nextRule
      }
      // 用于最后一次调用,因为每次校验都会调用check方法,这里结尾时重写check方法
      end() {
        this.nextRule = {
          check() {
            console.log('校验成功')
          }
        }
      }
    }

    btn.addEventListener('click', () => {
      const check = new CheckChain(checkEmpty)
      // 直接链式调用即可
      check.addRule(checkIsNumber).addRule(checkLength).end()
      check.check()
    })
  </script>
</body>
</html>

三、总结

有些设计模式我们平时有所接触,但有些设计模式比较陌生。上述的设计模式代码实现还是有一定难度的,值得好好去理解一下,我也在不断学习中,各位看客如果觉得本文对你有帮助,可以点赞收藏,希望在前端路上可以和大家一起慢慢进步成长。