• 什么是单例模式?
  • 单例模式的实现。
  • JavaScript中的单例模式。

什么是单例模式?

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

单例模式的实现

根据单例模式的特点,一个类仅有一个实例。实现单例模式无非通过一个标志变量判断该类是否创建过对象

  • 如果创建过,就返回已创建的对象
  • 否则,创建一个新对象并返回。
    1. // 一个简单的单例模式
    2. let Singleton = function(name) {
    3. this.name = name
    4. this.instance = null
    5. }
    6. Singleton.getInstance = function(name) {
    7. if(!this.instance) {
    8. this.instance = new Singleton(name)
    9. }
    10. return this.instance
    11. }
    12. let singleton = Singleton.getInstance('bayue')
    13. let singleton1 = Singleton.getInstance('bayue01')
    14. console.log(singleton === singleton1); // true
    以上是单例模式的一个简单实现,通过SingletongetInstance()方法返回该对象的实例。在方法内部判断该对象上的instance属性(标志变量)是否为null,如果是null,则创建一个新的对象并返回。
    1. // 单例模式的另一种写法
    2. // ...
    3. Singleton.getInstance = (function () {
    4. let instance = null
    5. return function(name) {
    6. if(!instance){
    7. instance = new Singleton(name)
    8. }
    9. return instance
    10. }
    11. })()
    12. // ...
    两种写法都可以实现单例模式,个人认为,第一种写法在Singleton实例中,始终有一个instance属性,而第二种写法,只有在需要时才会声明instance变量(惰性单例)。但第一种写法看起来更直观一些。

透明的单例模式

通过上述实现,使用者必须知道该类是一个单例类,而且获取它的实例也需要用getInstance(),增加了这个类的不透明度
因此需要让该类在使用时和普通的类一样去使用。

  1. let CreateDiv = (function() {
  2. let instance = null
  3. let CreateDiv = function(html) {
  4. if(instance) {
  5. return instance
  6. }
  7. this.html = html
  8. this.init()
  9. return instance = this
  10. }
  11. CreateDiv.prototype.init = function() {
  12. let div = document.createElement('div')
  13. div.innerHTML = this.html
  14. document.body.appendChild(div)
  15. }
  16. return CreateDiv
  17. })()

这是一个透明的单例模式,但是该类的内部为了将instance封装,明显增加了程序内部的复杂度,而且函数内部的也不符合单一职责原理,必然会造成高耦合
如果未来需求要求将该类也能变成可以有多个实例的类时,就不得不去修改这个类本身(有可能会造成未知的bug),或者单独写一个应用于普通场景的类(代码冗余)。

利用代理实现单例模式

上述透明的单例模式存在的问题主要是:

  • 函数内部不同功能耦合度高
  • 类的可复用性差

通过代码可以看到CreateDiv函数其实只负责两件事,一是检查该类的实例是否存在,二是初始化init()
CreateDiv只负责初始化逻辑,成为一个普通的类。

  1. // CreateDiv在这里仅作为一个普通的类。
  2. let CreateDiv = function(html) {
  3. this.html = html
  4. this.init()
  5. }
  6. CreateDiv.prototype.init = function() {
  7. let div = document.createElement('div')
  8. div.innerHTML = this.html
  9. document.body.appendChild(div)
  10. }

如果要实现单例模式的话,通过代理的方式,来间接将作为普通类的CreateDiv变为单例的类。

  1. let ProxySingleton = (function() {
  2. let instance = null
  3. return function(html) {
  4. if(!instance) {
  5. instance = new CreateDiv(html)
  6. }
  7. return instance
  8. }
  9. })()

如果需要多个实例时,使用CreateDiv就可以实现,如果需要将CreateDiv变为单例类,使用ProxySingleton类即可。

以上单例模式的代码更接近于面向对象语言中的实现。但JavaScript中并没有类的概念(在ES6中新增的class关键字,其实只是一个语法糖),所以下面实现JavaScript中的单例模式。

JavaScript中的单例模式

根据单例模式的核心概念:确保只有一个实例,并提供全局访问点
在JavaScript中,一个全局变量就满足单例模式的特点。

  1. // 只有一个变量a
  2. // 作为全局变量提供了全局的访问点
  3. let a = {}

上述代码使用了ES6新增的关键字let,如果用ES5中var来声明变量,则很容易不经意间将变量覆盖
为了确保全局变量的唯一和降低命名污染,有以下几种方式:

  1. 使用命名空间 ```javascript let namespace = { a: function() {}, b: function() {} } // 动态创建命名空间 let MyApp = {}

MyApp.namespace = function(name) { let parts = name.split(‘.’) // [a, b] let current = MyApp for(let i in parts) { if(!current[parts[i]]) { current[parts[i]] = {} } current = current[parts[i]] } } MyApp.namespace(‘event’) MyApp.namespace(‘dom.style’)

  1. 2. 使用**闭包封装**私有变量
  2. ```javascript
  3. let User = (function () {
  4. let __name, __age
  5. return {
  6. getUserInfo: function() {
  7. return __name + '-' + __age
  8. }
  9. }
  10. })()

惰性单例

惰性单例是指,在需要时才会创建对象实例。在单例模式的第二种实现中,就使用过惰性单例。
在实际应用中,常见的就是模态框的创建。通常来说,一个页面只会存在于一个模态框(不考虑多级操作的情况),所以这种场景就可以通过单例模式来实现。

  1. let CreateModal = (function () {
  2. let modal = null
  3. return function (content) {
  4. if(!model) {
  5. modal = document.createElement('div')
  6. modal.innerHTML = content || ''
  7. modal.style.display = 'none'
  8. document.body.appendChild('div')
  9. }
  10. return model
  11. }
  12. })()

通用的惰性单例

上述代码实现了一个模态框场景的惰性单例模式。但这段代码依然存在不少问题。

  • 违反单一职责原则,在CreateModal中将创建元素单例模式的判断都放在了一个函数中。
  • 扩展性不够,如果未来需要创建其他元素,就不得不cv这段代码,造成代码冗余

    实际上,模式就是将不变的部分和可变的部分分离的过程。

观察上述代码逻辑可知,主要分为两个部分:

  • 管理单例的逻辑
  • 创建对象的逻辑

管理单例的函数

  1. /**
  2. * @param fn 创建对象的函数,createDiv、createImg等
  3. */
  4. let getSingle = function(fn) {
  5. let result = null
  6. return function () {
  7. return result || result = fn.apply(this, arguments)
  8. }
  9. }

创建div对象的方法

  1. let createDiv = function (content) {
  2. let div = document.createElement('div')
  3. div.innerHTML = content
  4. div.style.display = 'none'
  5. document.body.appendChild('div')
  6. console.log('run') // 因为是单例模式,所以只执行一次
  7. return div
  8. }

最终代码如下

  1. let createSingle = getSingle(createDiv)
  2. // click函数
  3. button.onclick = function () {
  4. createSingle()
  5. }