单例模式

170
  • 单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点
  • 原理:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象

1. 单例模式

//第一种写法
var Singleton1 = function (name) {
  this.name = name;
  this.instance = null;
};

Singleton1.getInstance = function (name) {
  if (!this.instance) {
    this.instance = new Singleton1(name);
  }
  return this.instance;
};

var a = Singleton1.getInstance('sven1');
var b = Singleton1.getInstance('sven2');

alert(a === b);   //true

//第二种写法

var Singleton2 = function (name) {
  this.name = name;
}

Singleton2.getInstance = (function () {
  var instance = null;
  return function (name) {
    if (!instance) {
      instance = new Singleton2(name);
    }
    return instance;
  }
})();

var c = Singleton2.getInstance('jun');
var d = Singleton2.getInstance('bin');

alert(c === d);   //true
  • 上面实现的缺点:这种方式相对简单,但有一个问题,就是增加了这个类的“不透明性”,Singleton 类的使用者必须知道这是一个单例类,跟以往通过new XXX 的方式来获取对象不同,这里偏要使用 Singleton.getInstance 来获取对象

2. 透明的单例模式

  • 我们现在的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使 用其他任何普通类一样。在下面的例子中,我们将使用 CreateDiv 单例类,它的作用是负责在页面中创建唯一的 div 节点
var CreateDiv = (function () {

  var instance;

  var CreateDiv = function (html) {
    if (instance) {
      return instance;
    }
    this.htme = html;
    this.init();

    return instance = this;
  };

  CreateDiv.prototype.init = function () {
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
  };

  return CreateDiv;

})();

var a = new CreateDiv('sven1');
var b = new CreateDiv('sven2');

console.log(a === b);//true
  • 缺点:为了把 instance 封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的 Singleton 构造方法,这增加了一些程序的复杂度

3.代理实现单例模式

var CreateDiv = function (html) {
  this.html = html;
  this.init();
};

CreateDiv.prototype.init = function () {
  var div = document.createElement('div');
  div.innerHTML = this.html;
  document.body.appendChild(div);
};

// 接下来引入代理类
var ProxySingtonCreateDiv = (function () {

  var instance;
  return function (html) {
    if (!instance) {
      instance = new CreateDiv(html);
    }
    return instance;
  }
})();

var c = new ProxySingtonCreateDiv('kobe');
var d = new ProxySingtonCreateDiv('curry');

console.log(c === d); //true

4. JavaScript中的单例模式

​ 前面提到的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在 Java 中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。但 JavaScript 其实是一门无类(class-free)语言,也正因为如此,生搬单例模式的概念并无意义。在 JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在 JavaScript 中并不适用。

  • 单例模式的核心是确保只有一个实例,并提供全局访问。
4.1 惰性单例
  • 惰性单例指的是在需要的时候才创建对象实例。(惰性单例是单例模式的重点)
//基于“类”的惰性单例模式
Singleton.getInstance = (function () {
  var instance = null;
  return function (name) {
    if (!instance) {
      instance = new Singleton(name);
    }
    return instance;
  };
})();
  • 案例:点击按钮的时候才创建dom,并且符合单例模式

 <html>
   <body>
     <button id="loginBtn">登录</button>
   </body>
  <script>

    var createLoginLayer = (function () {
      var div;
      return function () {
      //这段代码仍然是违反单一职责原则的,
      //创建对象和管理单例的逻辑都放在
      //createLoginLayer对象内部
        if (!div) {
          div = document.createElement('div');
          div.innerHTML = '我是登录浮窗';
          div.style.display = 'none';
          document.body.appendChild(div);
        }
        return div;
      };
    })();


    document.getElementById('loginBtn').onclick = function () {
      var loginLayer = createLoginLayer();
      loginLayer.style.display = 'block';
    };

   </script>
 </html>

​ 我们需要把不变的部分隔离出来,先不考虑创建一个 div和创建一个 iframe 有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象 。

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

var createLoginLayer = function () {
  var div = document.createElement('div')
  div.innerHTML = '我是登录浮窗'
  div.style.display = 'none'
  document.body.appendChild('div')
  return div
}

var createSingleLayer = getSingle(createLoginLayer)

document.getElementById('loginBtn').onclick = function () {
  var loginLayer = createLoginLayer()
  loginLayer.style.display = 'block'
}

// 下面我们再试试创建唯一的 iframe 用于动态加载第三方页面:
var createSingleIframe = getSingle(function () {
  var iframe = document.createElement('iframe')
  document.body.appendChild(iframe)
  return iframe
})
document.getElementById('loginBtn').onclick = function () {
  var loginLayer = createSingleIframe()
  loginLayer.src = 'http://baidu.com'
}