[设计模式]——单例模式

158 阅读4分钟

定义

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

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象。在JavaScript开发中,单例模式的用途同样非常广泛,当我们点击登录按钮时,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单词多少次,这个浮窗只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

实现单例模式

要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

代码如下:

let Singleton=function(name){
    this.name=name
}

Singleton.prototype.getName=function(){
    return this.name
}

//用一个变量标志当前这个类是否被创建
Singleton.instance=null
Singleton.getInstance=function(name){
    if(!this.instance){
        this.instance=new Singleton(name)
    }
    return this.instance
}

let a=Singleton.getInstance("a")
let b=Singleton.getInstance("b")

console.log(a===b)//true

正常来说,一个类每次实例化生成的对象是不同的,在上述代码中使用getInstance获取实例化生成的对象并赋值给ab,打印true说明单例模式成果。

也可以不把生成的实例化对象instance挂载到类的属性上。

Singleton.getInstance=(function(){
    let instance
    return function(name){
        if(!instance){
            instance=new Singleton(name)
        }
        return instance
    }
})()

IIEF创建一个闭包,把instance存储在闭包中,每次执行getInstance时进行判断是否已经实例化过。

透明的单例模式

使用getInstance来获取Singleton类的唯一对象,这种方式相对简单,但是增加了这个类的不透明性

Singleton类的使用者必须知道这是一个单例类,跟以往通过new xxx的方式来获取对象有所不同,如果要实例化对象,要使用getInstance的方法,故要实现一种透明的单例模式

现有一个需求,使用createDiv单例类,它的作用是在页面中创建唯一的div节点。

//createDiv
let createDiv=(function(){
    let instance

    let createDiv=function(html){
        if(instance){
            return instance
        }
        this.html=html
        this.init()
        return instance=this
    }

    createDiv.prototype.init=function(){
        console.log('init...')
    }
  
    return createDiv
})()

let a=new createDiv('html1')
let b=new createDiv('html2')
console.log(a===b)//true,且只打印了一次'init...'

虽然完成了一个透明的单例类,但存在一些缺点。

    let createDiv=function(html){
        if(instance){
            return instance
        }
        this.html=html
        this.init()
        return instance=this
    }

在这段代码中,负责了两件事情,第一是创建对象和执行初始化init方法,第二是保证只实例化一个对象。

这违背了设计模式中的“单一职责原则”,即一段代码中负责了两种职责。

假如某天需要利用这个类,在页面中创建千千万万个div,即要让这个类从单例类变成一个普通的可产生多个实例的类,那么就必须改写createDiv构造函数,把控制创建唯一对象那一段代码去掉,这种修改会带来不必要的麻烦。

下面用代理模式实现单例化。

用代理实现单例模式

首先在createDiv构造函数中,把负责管理单例的代码移除,使它成为一个普通的创建div的类:

//代理实现
var createDiv = function (html) {
  this.html = html;
  this.init();
};

createDiv.prototype.init = function () {
  console.log("init...");
};

接下来引入代理类proxySingletonCreateDiv

var ProxySingletonCreateDiv = (function () {
  let instance;
  return function (name) {
    if (!instance) {
      instance = new createDiv(name);
    }
    return instance;
  };
})();

let a=new ProxySingletonCreateDiv('html1')
let b=new ProxySingletonCreateDiv('html2')
console.log(a,b,a===b)//true

通过引入代理类的方式,把负责管理单例的逻辑迁移到了代理类proxySingletonCreateDiv中,这样一来,createDiv就变成了一个普通的类,它与proxySingletonCreateDiv组合起来可以达到单例模式的效果。

惰性单例

惰性单例指的是需要的时候才创建对象实例。

惰性单例是单例模式的重点,在一开始的例子中就运用到了这种技术。instance实例对象总是在我们调用Singleton.getInstance的时候才创建,并不是在页面加载好的时候才创建。

假设有这么一个网站,当点击右上角登录时,会弹出一个登录浮窗,很明显这个浮窗在页面中是唯一的,不可能同时存在两个登录窗口的情况。

假如希望可以在进入页面的时候,这个浮窗节点先不创建,而等用户点击登录时才创建,达到惰性效果,那么:

<body>
  <button id="loginBtn">login</button>
    <script>
        var createLoginLayer = function () {
            var div = document.createElement('div')
            div.innerHTML = 'i am loginLayer'
            div.style.display = 'none'
            document.body.appendChild(div)
            return div
        }

        document.getElementById("loginBtn").onclick = function () {
            var loginLayer = createLoginLayer()
            loginLayer.style.display = 'block'
        }
    </script>
</body>

这样虽然达到惰性目的,但每次点击登录按钮时都会创建一个新的登录浮窗div

这里使用单例模式创建浮窗

<body>
    <button id="loginBtn">login</button>
    <script>
        var getSingle = function (fn) {
            let result
            return function(){
                return result||(result=fn.apply(this))
            }
        }

        var createLoginLayer = function () {
            div = document.createElement('div')
            div.innerHTML = 'i am loginLayer'
            div.style.display = 'none'
            document.body.appendChild(div)
            return div
        }

        var createSingleLoginLayer=getSingle(createLoginLayer)

        document.getElementById("loginBtn").onclick = function () {
            var loginLayer = createSingleLoginLayer()
            loginLayer.style.display = 'block'
        }
    </script>
</body>

这个例子中,也是把创建实例对象的职责和管理单例的职责放在两个方法里面,两个方法独立互不影响,当他们连在一起时就实现了单例模式的功能。

ES6实现单例

class Singleton{
    constructor(name){
        this.name=name
        this.init()
    }

    init(){
        console.log('init..')
    }
}

const getProxySingleton=function(){
    let instance
    return class{
        constructor(name){
            if(instance) return instance
            instance=new Singleton(name)
            return instance
        }
    }
}

const ProxySinleton=getProxySingleton()
let a=new ProxySinleton('name1')
let b=new ProxySinleton('name2')
console.log(a,b)