JS常见设计模式 之 单例模式

219 阅读4分钟

什么是单例模式?

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

单例模式是一种很常用的模式,我们很多不了解设计模式的小伙伴可能日常开发中用到了,但是并没有意思到这是一个设计模式。对于一些对象我们通常只需要一个,比如线程池、全局缓存、浏览器的window对象。而在JavaScript中单例模式同样运用的十分广泛,比如Element库中的Dialog,加入我们用它来实现一个弹窗组件,我们就会发现第二次点击后会复用上一次出现的弹窗,无论点击多少次出现的都是同一个弹窗,那么我们就可以说这个弹窗是用单例模式实现的。

如何实现一个单例模式

我们可以先来实现一个简单的单例模式,比如创建一个弹窗,我们取用一个标志位标志当前的单例是否被创建过,如果是那么就将其创建过的实例返回,如果不是则重新创建。代码如下:

    // 创建一个div
    var CreateDiv = (function() {
      let instance
      let CreateDiv = function(html) {
        if (instance) {
          return instance
        }
        this.html = html
        this.init()
        return instance = this
      }

      CreateDiv.prototype.init = function() {
        let div = document.createElement('div')
        div.innerHTML = this.html
        document.body.appendChild(div)
      }
      return CreateDiv
    })()
    let div1 = new CreateDiv('div1')
    let div2 = new CreateDiv('div2')
    console.log(div1 === div2) // true

但是这种写法是存在问题的,控制单例和执行初始化方法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)
    }

    const CreateDivProxy = (function() {
      let instance
      return function(html) {
        if(!instance) {
          instance = new CreateDiv(html)
        }
        return instance
     }
    })()

js中的单例模式

其实我们在全局变量中定义一个变量比如var a = {},这个变量a就是一个符合单例模式的例子,a是唯一的并且是能够全局访问的。

但是这样写会造成一个很严重的问题:那就是全局污染,现在我们大多数写的代码都是通过框架(vue、react)去写的代码,由于组件化开发的模式使得我们在日常开发中很少能碰到变量污染的情况。如果我们有维护一些计较老的项目(jsp+jq)的话,就很可能会遇到这类问题,这个时候我们就需要对变量去进行封装标记。

    const namespace = {
      a: '命名空间写法',
      b() {},
      c: () => {}
    }
    
    // or
    
    const scope = (function() {
      const a = '闭包封装变量'
      const b = () => {}
      const c = {}
      return {
        getScopeInfo() {
          a,
          b,
          c
        }
      }
    })()

惰性单例

相信大家看了前面对于单例的介绍已经对单例模式有了一个简单的了解,接下来看一个在js中十分常用的一种单例模式:惰性单例

惰性单例指的是:在需要的时候才去进行创建的单例。在我们日常开发中有很多这种例子,比如el-dialog。这种模式对于我们实际开发而言是非常有用的,因为我们通常使用el-dialog的时候不是一进页面就需要对其进行使用的,而是在用户进行了操作之后才显示的,如果用户没有触发条件这个实力就永远不会创建。

  const CreateDialog = (function() {
    let dialog
    return function() {
      if (!dialog) {
        dialog = document.createElement('div')
        dialog.innerHTML = '我是一个dialog'
        dialog.style.display = 'none'
        document.body.appendChild(dialog)
      }
      return dialog
    }
  })()

  document.getElementById('showDialog').onclick = function() {
    let dialog = new CreateDialog()
    dialog.style.display = 'block'
  }

或者我们可以更进一步的将单例的逻辑抽离出来,进行逻辑解耦:

  const CreateDialog = function() {
    const dialog = document.createElement('div')
    dialog.innerHTML = '我是一个dialog'
    dialog.style.display = 'none'
    document.body.appendChild(dialog)
    return dialog
  }

  const getSingle = function(fn) {
    let instance
    return function() {
      return instance || (instance = new fn())
    }
  }

  const dialogSingle = getSingle(CreateDialog)

  document.getElementById('showDialog').onclick = function() {
    let dialog = dialogSingle()
    dialog.style.display = 'block'
  }

总结

单例模式最最最核心的概念就是保证一个类仅有一个实例,并提供一个访问它的全局访问点,对于其他语言实现起来可能比较麻烦,但是对于JS来说由于语言特性实现起来是非常简单的,主要是靠高阶函数和闭包的概念去进行实现。

惰性单例是一个JS中经常用的到并且非常实用的单例模式,不过我们在设计单例的时候需要考虑到解耦的情况,这样以后在遇到其他情况的时候才好去进行解耦。