likes
comments
collection
share

前端JavaScript中常见设计模式

作者站长头像
站长
· 阅读数 24

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

本文主要来学习前端常用的设计模式。设计模式是软件设计中的一种思维方式,并不是某种语言所独有的,javaC 等各种语言都可以遵循这些设计模式来进行代码的编写。

作为一名前端开发人员,可以使用 JavaScript 语言来将这些设计模式呈现出来,在工作中也能够应用上。

可以进一步将设计模式拆分为 设计原则设计模式

设计原则

设计原则:软件设计要遵循的原则。

  • 单一职责原则:每个类或方法只做一件事
  • 开闭原则:对外开放,对内关闭,尽量少的去更改底层的功能,将方法暴露给外部去修改
  • 里氏替换原则:子类继承父类,尽量不要去修改覆盖父类的方法
  • 迪米特法则:不推荐跨层级通信(如vue中爷孙组件通信时,可以使用Vuex传递数据,不建议直接跨层级通信)
  • 接口隔离原则:接口设计最小单元,高内聚、低耦合
  • 依赖倒置原则:抽象出来的方法、类应该是共性的内容,而不是很细节的内容

设计模式

设计模式 是软件开发人员在开发过程中面临一些具有代表性问题的解决方案,也可以说是我们的前辈在踩了一些坑之后,为我们总结出来的一些解决问题的通用方案。

遵循设计模式编写出来的代码 复用性可扩展性 会更加明显,利于维护。

设计模式设计规则具体化,通常分为三大类:

  • 创建型
  • 结构性
  • 行为型

常见的 设计模式 有以下几种:

  1. 单例模式
  2. 工厂模式
  3. 装饰器模式
  4. 观察者模式
  5. 代理模式
  6. 适配器模式
  7. .......

这些模式都属于上面提到的三大类中的某一类型。下面来具体学习一下这些设计模式。

单例模式

单例模式(Singleton Pattern) 属于创建型的一种常用的软件设计模式,是指全局同时只能有一个实例,也要保证一个类只能有一个实例,并要提供全局可调用的接口。这样就算实例化多次,也不会创建多个实例,可以节约性能。我们常见的 window document 以及 vue 中的 Store 都是单例模式。

实现一个简单的单例模式:

// 利用静态属性值 instance 实现单例模式
    class Person {
      static instance
      constructor(name) {
        if(!Person.instance) {
          Person.instance = this
        } else {
          return Person.instance
        }
        this.name = name
      }
    }
    let ci = new Person('cilly')
    let to = new Person('tom')
    console.log(ci, to)
    console.log(ci===to) // true

如果有多个类都要实现单例模式的话,需要在每个类中都定义上面这样的静态属性,会显得很麻烦。这时候可以编写一个通用方法来实现:

// 比如要将下面动物类和水果类变为单例模式
    class Animal{
      constructor(name) {
        this.name = name
      }
    }
    class Fruit{
      constructor(name) {
        this.name = name
      }
    }
    // 通用创建单例模式的方法具体实现
    function createSingle(Fn) {
      let instance // 通过闭包缓存 instance
      return function(...args) {
        if(!instance) {
          instance = new Fn(...args)
        }
        return instance
      }
    }
    // 使用
    const singleAnimal = createSingle(Animal)
    const dog = new singleAnimal('dog')
    const pig = new singleAnimal('pig')
    console.log(dog, pig)
    console.log(dog === pig) // true

利用闭包来缓存 instance,就不用在每个类里面都定义一个静态属性。

单例模式的应用场景:

下面创建一个弹窗,要求控制每个实例只能有一个弹窗,并且全局范围内只能有一个实例。利用单例模式来解决:

    // 弹窗
    class Dialog {
      constructor() {
        this.dialog = document.createElement('div')
        this.dialog.innerHTML = '弹窗'
        this.dialog.style.display = 'none'
        this.isShow = false
      }
      show() {
        if(!this.isShow) {
          document.body.append(this.dialog)
          this.dialog.style.display = 'block'
          this.isShow = true
        } else {
          console.log('当前存在弹窗')
        }
      }
    }
    
    // 使用
    const dialog = new Dialog()
    document.querySelector('button').onclick = function() {
      dialog.show()
    }

这样 实现了一个类只创建一次弹窗,但是如果实例化两次就能创建两个弹窗,如下:

    // 使用
    const dialog = new Dialog()
    const dialog2 = new Dialog()
    document.querySelector('button').onclick = function() {
      dialog.show()
      dialog2.show()
    }

解决思路是:通过 createSingle 方法来限制全局范围内只能有一个实例,这样就实现了每个实例只能有一个弹窗,并且全局范围内只能有一个实例。如下:

    // 使用
    // const dialog = new Dialog()
    // const dialog2 = new Dialog()
    const createDialog = createSingle(Dialog)
    const dialog = new createDialog()
    const dialog2 = new createDialog()
    document.querySelector('button').onclick = function() {
      dialog.show()
      dialog2.show()
    }

单例模式优缺点

  • 优点:节约内存开支和实例化时的性能开支
  • 缺点:扩展性不强

工厂模式

工厂模式(Factory Pattern):封装具体实例创建过程和逻辑,外部可以根据不同的条件创建不同的实例。

工厂模式的简单示例:

    class Dog {
      constructor() {
        this.name = '旺旺'
      }
    }
    class Cat {
      constructor() {
        this.name = '喵喵'
      }
    }
    // 工厂模式
    function Factory(key) {
      switch (key) {
        case 'Dog':
          return new Dog()
          break
        case 'Cat':
          return new Cat()
          break
        default:
          break
      }
    }

    // 使用
    const ww = Factory('Dog')
    const mm = Factory('Cat')
    console.log(ww, mm) // Dog {name: '旺旺'} Cat {name: '喵喵'}
    console.log(ww.name, mm.name) // 旺旺 喵喵

工厂模式优缺点

  • 优点:功能封装后代码具有复用性,抽象逻辑
  • 缺点:增加代码复杂度

装饰器模式

装饰器(或装饰者)模式(Decorator Pattern):允许向一个现有的对象添加新的功能,同时又不改变其结构。

简单示例1:

    class Person {
      constructor() {
        this.name = '张三'
      }
      song() {
        console.log(' 唱')
      }
    }
    // 装饰器
    Person.prototype.Decorator = function(fn) {
      this.song()
      fn.apply(this)
    }

    const zs = new Person()
    // zs.song()
    function jump() {
      console.log(this.name + ' 跳');
    }
    zs.Decorator(jump)

示例2:通过给 Function 添加装饰方法,形成装饰者链

    // 装饰者链
    Function.prototype.Decorator = function(fn) {
      // 返回一个函数以便链式调用
      return () => {
        this()
        fn()
      }
    }
    function rap() {
      console.log('rap')
    }
    function talk() {
      console.log('talk')
    }
    // 链式调用
    zs.song.Decorator(rap).Decorator(talk)()

示例3:对 window.onload 进行装饰

    // 对 window.onload 进行装饰
    window.onload = function() {
      console.log('----onload');
    }
    const _onload = window.onload

    window.onload = function() {
      _onload()
      console.log('这里做额外处理de一些事情...');
    }

观察者模式

观察者模式(Observer Pattern):对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

addEventListener 的事件监听机制,底层就是一个观察者模式。

<button>点击</button>
    document.querySelector('button').addEventListener('click', function(){
      console.log('click1');
    })
    document.querySelector('button').addEventListener('click', function(){
      console.log('click2');
    })

下面用观察者模式模仿系统的addEventListener

    class MyEvent {
      constructor() {
        this.handles = {} // 数据格式:{ en1: [fn1, fn2 ...], en2: [fn1, fn2 ...] }
      }
      addEvent(eventName, fn) {
        if(typeof this.handles[eventName] === undefined) {
          // handles 中不包含eventName时
          this.handles[eventName] = []
        }
        this.handles[eventName].push(fn)
      }
      trigger(eventName) {
        if(!(eventName in this.handles)) {
          return
        }
        this.handles[eventName].forEach(fn => {
          fn()
        })
      }
    }

使用这个类:

    const obj = new MyEvent()
    obj.addEvent('event1', function(){
      console.log('event1')
    })
    obj.addEvent('event1', function(){
      console.log('event1---')
    })
    // 模拟obj变化时,触发监听的函数
    setTimeout(function(){
      obj.trigger('event1') // event1 event1---
    },2000)

代理模式

代理模式(Proxy Pattern):为其他对象提供一种代理,控制对这个对象的访问。

示例:

实现一个创建图片的类,并向网页中插入一张图片

    class CreateImage {
      constructor() {
        this.img = document.createElement('img')
        document.querySelector('body').append(this.img)
      }
      setSrc(src) {
        this.img.src = src
      }
    }

    let img = new CreateImage()
    img.setSrc('https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/haidi.ffe5bcf.png')
  

网络较慢或图片较大时,图片可能加载的很慢。这时可以添加一个loading动画,优化用户体验,这里就可以用到代理模式

    function proxyImg(src) {
      let img = new CreateImage()
      let loadingImg = new Image()
      img.setSrc('./img/loading.gif') // 先显示loading
      loadingImg.src = src // 准备加载真实图片
      // 图片加载完成
      loadingImg.onload = function() {
        img.setSrc(src) // 把loading替换掉
      }
    }

    proxyImg('https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/haidi.ffe5bcf.png')

注意:代理模式要对代理对象做一些控制,否则什么都不做就没有意义了

适配器模式

适配器模式(Adapter Pattern):是作为两个不兼容的接口之间的桥梁。不兼容的接口或数据等,通过适配器的转换,可以得到期待的结果

示例:

    function getList() {
      return [
        {
          name: 'cilly',
          age: 10
        },
        {
          name: 'tom',
          age: 20
        }
      ]
    }

    // 适配器
    function Adapter(arr) {
      let res = []
      for(let i = 0; i < arr.length; i++) {
        const obj = {
          [arr[i].name] : arr[i].age
        }
        res.push(obj)
      }
      return res
    }

    let res = Adapter(getList())
    console.log(res) // [ {cilly: 10}, {tom: 20} ]

适配器模式优缺点

  • 优点:1. 使用适配器解决兼容问题,不用修改原有逻辑,让原有逻辑可以更好的服用。2. 灵活性好,适配器不会对原有的功能造成影响,不想使用适配器时直接删掉即可。

  • 缺点:过多使用适配器,会使系统变的零乱,代码复杂度增加

未完待续 ......

代码地址:github.com/kongcodes/j…