JS设计模式之单例模式

680 阅读7分钟

前言

我最近在学习JavaScript设计模式,写文章是为了帮助我自己巩固知识,顺带着也可以分享一下自己的学习内容和学习心得。之前我一直以为设计模式应该很难吧,我一定学不会。直到寒假看了这本小册子:JavaScript 设计模式核⼼原理与应⽤实践之后我发现其实设计模式并没有那么高深,反而平时都在用,只不过不知道这就叫设计模式。在学习这本小册子的过程中,没有太吃力,反而整个过程是一次又一次的顿悟:“哦,这个东西原来我平时用过,原来他叫XXXX”。当然这本小册子只是入个门,作者也在小册子中鼓励我们要开拓眼界,扎实自己的编码能力。所以我又去买了一本书叫做《JavaScript设计模式与开发实践》,希望自己能学到东西。这篇文章的写作背景就是我在读完这本书中的单例模式那一章所写下的。

单例模式的基本思路

单例模式的定义就是:

一个类仅有一个实例,并提供一个它的全局访问点
直接看定义是不是有点不太明白,先来看一个最简单的单例模式实现

function SingleDog (name) {
  this.name = name
}
// 获取实例的方法
SingleDog.getInstance = (function () {
  let instance = null
  return function () {
    // 通过闭包保存实例,这样就可以判断是否已经实例化过了
    if (!instance) {
      return instance = new SingleDog([].shift.apply(arguments))
    }
    return instance
  }
})();
let dog1 = SingleDog.getInstance('Jack')
let dog2 = SingleDog.getInstance('Jack2')
console.log(dog1 === dog2)

通过代码不难看出,实现单例模式的关键就是需要判断该类是否已经实例化过对象了。 至于说具体的代码方式有很多,像我这里的闭包可以实现,除此之外,ES6中的class也可以。在上面的代码中我们只能通过SingleDog.getInstance方法去获取SingleDog类的唯一对象,这其中存在着“不透明”的问题,什么叫不透明,就是使用者需要知道SingleDog是一个单例类,并且只能通过SingleDog.getInstance这种方式来获取对象。如果直接使用new调用构造函数那么SingleDog就不再是一个单例类了。

透明的单例模式

上面提到了“不透明”的问题,其实解决方法很简单,只需要对整个SingleDog进行更深层次的封装就可以了。

let SingleDog = (function () {
  let instance = null
  let SingleDog = function (name) {
    this.name = name
    if (instance) {
      return instance
    }
    return instance = this
  }
  return SingleDog
})()

console.log(new SingleDog() === new SingleDog())  // true

为了封装instance,使用了自执行函数和闭包。自执行函数返回了真正的SingleDog实例的构造函数,在这个构造函数中,判断是否已经实例化过了。由于是闭包,所以instnace这个变量不会被销毁。
这样在使用这个类实例化对象时,就不需要提前知道它是一个单例类了,和普通类一样可以通过new操作符调用。让这个单例类足够“透明”。

JavaScript中的单例模式

上面的两种单例模式的实现,都比较倾向于传统面向对象语言中的实现,单例对象必须从“类”中创建而来,这在以“类”为中心的语言中是一件很自然的事情。
但是JavaScript是一门无类(class-free)语言,这就代表,直接将设计模式生搬硬套到JavaScript中是不合适的。在JavaScript中创建对象的方法非常多,不一定需要构造函数的。既然我们只需要一个唯一的对象,那为什么还要去创建一个“类”呢?这无异于脱裤子放屁————多此一举。

惰性单例

惰性单例就是指在需要的时候才创建实例对象,惰性是单例模式中非常重要的一个技术点。其实从文章开头就一直在使用惰性单例。只不过是基于“类”的单例模式,基于类的单例模式在JavaScript中并完全不适用。
假设页面中有一个模态框可以通过点击按钮打开模态框,不希望频繁的删除或者增加节点,只通过修改样式控制其显示和隐藏。这时候就需要用到惰性单例。

.model{
  width: 300px;
  height: 150px;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid black;
}
<button class="btn">弹出对话框</button>
const createModel = (function (content) {
  let instance = null
  return function () {
    if (instance) {
      return instance
    }
    const div = document.createElement('div')
    div.className = 'model'
    div.innerHTML = arguments[0]
    div.style.display = 'none'
    document.body.appendChild(div)
    // 返回的是一个DOM节点,不再是某个类型的对象
    // 这里我们只需要一个唯一的 div,完全没必要创建一个“类”
    // 这也就是说为什么基于“类”的单例模式在JavaScript中并不完全适用
    return instance = div
  }
})()

const btn = document.querySelector('.btn')
btn.addEventListener('click', function () {
  // 每次点击按钮都是同一个div DOM节点
  const model = createModel('我是一个模态框')
  model.style.display = 'block'
})

通用的惰性单例

上一段代码,我们实现了一个可用的惰性单例,但它存在着如下问题

  • 这段代码显然是违反“单一职责原则”的,创建对象和管理单例的逻辑都写在了createModel
  • 如果我们下次需要创建页面中唯一iframe或者script用来跨域请求数据,那不得如法炮制把createModel函数重新抄一遍? 所以,我们需要分清楚哪些是变化的,哪些是不变的。把不变的部分隔离起来。在我们这个需求当中,先不管创建一个iframe或者script它们之间有什么差异,就管理单例这部分的逻辑是完全可以抽离出来的。
    现在把如果管理单例这一部分的代码抽离出来,封装到getSingle函数内部,将创建对象的方法fn当作参数传入函数
function getSingle(fn) {
  let result = null
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}

接下来将用于创建模态框的函数作为fn传入getSingle函数,接着让getSingle函数返回一个新函数,函数中用result变量保存fn的计算结果,由于是闭包result变量不会被销毁,在将来的请求中,如果result变量有值了就直接返回。

const createModel = function (content) {
  const div = document.createElement('div')
  div.className = 'model'
  div.innerHTML = arguments[0]
  div.style.display = 'none'
  document.body.appendChild(div)
  return instance = div
}

function getSingle(fn) {
  let result = null
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}

const singelModel = getSingle(createModel)
const btn = document.querySelector('.btn')
btn.addEventListener('click', function () {
  const model = singelModel('我是一个模态框')
  model.style.display = 'block'
})

在这个例子中,我们将创建对象的职责和管理单例的逻辑分开放置在两个方法中,这样这两个方法可以独立变化而互不影响。当它们连在一起的时候,就完成了创建唯一对象的功能,就问你妙不妙?这可能就是JavaScript独特的魅力吧!
假如我们要创建一些其他的单例,就正常的写逻辑就好了,如何管理单例直接交给getSingel函数就可以了

总结

这篇文章先讲了单例模式的基本思路,就是要判断一个类是否已经被实例化。由于使用getSingle方法获取唯一对象的方式不“透明”,所以我们将整个构造函数进行封装,使用自执行函数和闭包返回真正的构造函数。惰性是单例模式非常重要的点,也就是需要对象时才创建一个唯一的对象。为了适应更多的情况我们将实际逻辑代码和管理单例的逻辑分离开来了,这样它们可以独立变化而不相互影响。增强了代码的可读性和可维护性。