TIP

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

# 实现单例模式

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

var Singleton = function (name) {
	this.name = name;
	this.instance = null
}

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

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

var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')

alert(a === b)

# 透明的单例模式

“透明”的单例类,指的是用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。
下面的例子是使用CreateDiv单例类,在页面中创建唯一的div节点

var CreateDiv = (function() {
	var instance;
	var CreateDiv = function (html) {
		if (instance) {
			return instance
		}
		this.html = 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')

alert(a === b) // true

上面这段代码的缺点:

  1. 为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的构造方法,增加了一些程序的复杂度,阅读起来也不是很舒服
  2. CreateDiv构造函数负责了两件事情。一个是创建对象和执行init方法,一个是保证只有一个对象,这不符合"单一职责"原则

# 用代理实现单例模式

为了解决上面提到的缺点,我们改造一下透明的单例模式中的例子,把负责管理单例的代码移出去,使它成为一个普通的创建div的类

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 ProxySingletonCreateDiv = (function(){
  var instance;
  return function(html){
    if (!instance ){
      instance = new CreateDiv( html );
    }
    return instance; 
  }
})();
var a = new ProxySingletonCreateDiv( 'sven1' ); 
var b = new ProxySingletonCreateDiv( 'sven2' );
alert( a === b );  //true

上面的代码同样也是缓存代理的应用之一,后面的章节会详细说明

# JavaScript中的单例模式

WARNING

之前讲解的单例模式都是基于"类"创建单例对象。但是Javascript是一门无类的语言,生搬上面的单例模式并无意义。传统的单例模式实现在JavasSript中并不适用。

单例模式的核心是确保只有一个实例,并提供全局访问

var a = {}

在JavaScript中,以上代码其实就可以符合单例模式的要求。但是这种写法会有很多问题,比如容易造成命名空间污染,很容易被不小心覆盖。 下面使用几种方式来降低全局变量带来的命名污染

# 使用命名空间

var myApp = {};
myApp.namespace = function (name) {
  var list = name.split(".");
  var current = myApp;
  for(var i in list){
    if(!current[i]){
      current[list[i]] = {};
    }
  	current = current[list[i]];
	}
}
myApp.namespace("dom.name");
myApp.namespace("person.class.row");
console.log(myApp);

# 使用闭包封装私有变量

关闭闭包的说明及案例可以查看闭包和高阶函数

# 惰性单例

TIP

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

惰性单例是单例模式的重点,这种技术在实际开发中非常有用。

应用场景:创建唯一的登录浮窗

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

var createLoginLayer = (function () {
	var div;
	return function () {
		if (!div) {
			var div = document.createElement( 'div' );
			div.innerHTML = '我是登录浮窗';
			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';
};

以上代码还存在两个问题

  1. 违反了单一职责原则
  2. 扩展性低

# 通用的惰性单例

为了解决上面例子存在的问题,改造一下代码

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 createSingleLoginlayer = getSingle(createLoginlayer);

document.getElementById("btn").addEventListener("click",function(){
	var loginlayer=createSingleLoginlayer("我是窗体");
  loginlayer.style.display="block";
})

# 总结

创建对象和管理单例的职责分开成两个方法,组合使用,威力无穷😂