JavaScript - 单例模式

179 阅读5分钟

单例模式属于创建模式的一种。通过单例模式创建的类只有一个实例。这种模式使得类的对象成为系统的唯一实例。而我们在系统中多次调用这个类的实例的时候,并不会去覆盖这个类,也可以我们在开发的过程中避免变量覆盖。

它的核心概念:限制类的实例化次数并且只能实例化一次,一个类只能存在一个实例,并提供一个全局的访问点。

举个例子,我们在系统中通过字面量去定一个对象 User,如果有其它的开发者去重复定义这对象的时候,就会把之前的对象进行覆盖,从而导致这个对象不能正确的使用,并还有一定耦合性。

var User = {
    name:"黑黑的脸蛋",
    age:"23"
}

如果我们用单例模式对这个对象进行封装,并让这个对象成为系统中的唯一对象,就不会出现变量覆盖的问题了。单例模式可以根据复杂度分为 简单版单例透明版单例代理单例以及惰性单例

简单单例

我们可以利用函数是系统中的一等公民定义,在函数属性下创建一个方法来管控单例创建出来的实例,让创建出来的实例作为唯一的实例。

function Singleton( name,age ) {
    this.name = name
    this.age = age
​
    // 管控单例创建的实例
    this.instance = null
}
​
Singleton.prototype.getUser = function(){
    return {
        name:this.name,
        age:this.age
    }
}
​
Singleton.getInstance = function( name,age ) {
    if( !this.instance ) {
        this.instance = new Singleton( name,age )
    }
    return this.instance
}
​
var SingletonUser = Singleton.getInstance( "黑黑的脸蛋","23" )

可以看出以上的代码中,我们在单例下定义了一个 getInstance 方法来管控单例,并在函数内部创建了一个 this.instance 属性收集创建出来的实例,让第一次创建出来的实例作为系统的唯一实例。

image.png

可以看到,我无论调用多少次 getInstance,它第一次创建的实例都不会有所变化。这就是单例模式中的限制类的实例化次数并且只能实例化一次,一个类只能存在一个实例 从而也解决了对象覆盖的问题。

但是简单单例依旧存在问题,就是不够透明。我们需要调用 getInstance 来创建单例,导致创建的过程太过复杂。而我们需要通过 new 的方法来创建实例,把过程进一步简单化。

透明单例

var Singleton = ( function() {
    var instance
​
    return function ( name,age ) {
        this.name = name
        this.age = age
        if( !instance ) {
            instance = this
        }
        return instance
    }
} )()
​
var SingletonUser = new Singleton("黑黑的脸蛋","23")

透明单例解决了简单版单例的创建实例的过程复杂度,并可以直接使用 new 操作符来直接创建实例。

但是透明单例依旧没有解决代码的耦合性,创建实例和管控实例代码糅杂在一起,不符合设计模式中的 "单一职责原则"。

代理单例

通过“代理”的形式,意图解决:将管理单例操作,与对象创建操作进行拆分,实现更小的粒度划分,符合“单一职责原则”

var Singleton = ( function() {
    var instance
​
    function User( name,age ) {
        this.name = name
        this.age = age
    }
    User.prototype.getName = function() {
        console.log( this.name );
    }
​
    return function ( name,age ) {
        if( !instance ) {
            instance = new User( name,age )
        }
        return instance
    }
} )()
​
var SingletonUser = new Singleton("黑黑的脸蛋","23")

惰性单例

惰性单例,意图解决:需要时才创建类实例对象。在考虑优化场景的时候,我们往往会去想到懒加载解决方案,从而去实现 “按需加载”,而惰性单例也是解决这种优化场景的问题。

var GetSingleton = function( fn ) {
    var instance
    return function() {
        if( !instance ) {
            instance = fn.apply( this,arguments )
        }
        return instance
    }
}
​
var createMessageAlert = function( message ) {
    var alertBox = document.createElement( "div" )
    alertBox.innerHTML = message
    alertBox.style.display = "none"
    alertBox.className = "login-alert"
    document.body.appendChild( alertBox )
    return alertBox
}
​
var createMessageAlert = GetSingleton( createMessageAlert )
document.getElementsByTagName("button")[0].addEventListener( "click",function() {
    // 点击多次只会产生一个弹窗
    var alertMessage = createMessageAlert("欢迎回来")
    alertMessage.style.display = "block"
} )

在上例的代码中,我们定义了 GetSingleton 方法来实现惰性单例的创建,并在调用的过程中只会操控它唯一的实例。如果还需要更多的实例,可以继续调用 GetSingleton 来产生对应的实例方法。

ES6 实现简单版单例

class Singleton {
​
    constructor( name,age ) {
        this.name = name
        this.age = age
    }
​
    static getInstance( name,age ) {
        if( !this.instance ) {
            this.instance = new Singleton( name,age )
        }
        return this.instance
    }
}
​
var SingletonUser = Singleton.getInstance( "黑黑的脸蛋","23" )

ES6 实现透明单例

class Singleton {
​
    constructor( name,age ) {
        this.name = name
        this.age = age
​
        if( !Singleton.instance ) {
            Singleton.instance = this
        }
        return Singleton.instance
    }
}
​
var SingletonUser = new Singleton( "黑黑的脸蛋","23" )

ES6 实现代理单例

class Singleton {
​
    constructor( name,age ) {
        if( !Singleton.instance ) {
            Singleton.instance = new User( name,age )
        }
        return Singleton.instance
    }
}
​
function User( name,age ) {
    this.name = name
    this.age = age
}
User.prototype.getName = function() {
    console.log( this.name );
}
​
​
var SingletonUser = new Singleton( "黑黑的脸蛋","23" )

ES6 实现惰性单例

class Singleton {
​
    constructor( fn ) {
        return function () {
            if( !Singleton.instance ) {
                Singleton.instance = fn.apply( this,arguments )
            }
            return Singleton.instance
        }
    }
}
​
var createMessageAlert = function( message ) {
    var alertBox = document.createElement( "div" )
    alertBox.innerHTML = message
    alertBox.style.display = "none"
    alertBox.className = "login-alert"
    document.body.appendChild( alertBox )
    return alertBox
}
​
var createMessageAlert = new Singleton( createMessageAlert )
document.getElementsByTagName("button")[0].addEventListener( "click",function() {
    // 点击多次只会产生一个弹窗
    var alertMessage = createMessageAlert("欢迎回来")
    alertMessage.style.display = "block"
} )

场景应用

单例模式可以解决维护一个全局实例实例对象,避免这个对象被其他代码覆盖。

  • 引用第三方库 ( 如 JQuery )
  • 弹窗
  • 全局状态管理器 store ( Vuex / Redux )

项目中引入第三方库时,重复多次加载库文件时,全局只会实例化一个库对象,如 jQuerylodashmoment。。。,其实它们的实现理念也是单例模式应用的一种:

// 引入代码库 libs(库别名)
if (window.libs != null) {
  return window.libs;    // 直接返回
} else {
  window.libs = '...';   // 初始化
}

Vuex 中实例创建

if (!Vue && typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}
​
function install (_Vue) {
  if (Vue && _Vue === Vue) {
    console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.')
    return
  }
  Vue = _Vue
}

总结

单例模式是创建型模式其中之一,这种模式可以让一个对象成为系统中的唯一实例,并且自身可以去自行创建。如果当这个实例创建完成,只能把这个实例作为它的唯一实例,当你去多次调用创建实例的过程中,他会“阻止”你想要再次生成的实例。而使得这个实例成为系统中的静态方法(类方法),所以你多次调用创建实例,会让这个过程变得毫无意义。