javascript设计模式(一)-单例模式(读书笔记)

最近在研读曾探大佬的著作《JavaScript设计模式与开发实践》——单例模式,并做了如下读书笔记总结和个人理解。

单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
用途:如线程池、全局缓存、浏览器中的window对象等,在JS开发中,如登录浮窗。

实现简单的单例模式

// 代码块1
var Singleton = function( name ){ 
	this.name = name; 
 	this.instance = null;
}; 
// 代码块2
Singleton.prototype.getName = function(){ 
 	return this.name 
}; 
// 代码块3
Singleton.getInstance = function( name ){ 
 	if ( !this.instance ){ 
 		this.instance = new Singleton( name ); 
 	} 
 	return this.instance; 
}; 
// 代码块4
var a = Singleton.getInstance( 'sven1' ); 
var b = Singleton.getInstance( 'sven2' ); 
console.log( a === b ); // true
console.log(a.getName() === b.getName()); // true

执行过程

  • 首先跳到代码块4,调用了两次Singleton.getInstance方法,进入代码块3,其相当于对象属性,在第一次调用时,此时的this就指向该对象Singleton,即是function( name ){ this.name = name; this.instance = null; };,因此在代码块3中,进入if判断,this.instanceundefined。此时就通过new操作符实例化对象,这时候的this.instance就被赋值为Singleton {name: 'sven1', instance: null},即为a的值。
  • 接下来进行第二次调用,此时的this还是指向对象Singletonthis.instance相当于Singleton.instance,非undefined,因此会跳过判断,直接返回Singleton {name: 'sven1', instance: null},即为b的值。
  • 所以a全等于b, 而a.getName()在对象本身上没有找到该方法,就会去原型上找,如果有,则返回,b类似且返回的内容均为sven1,因此它两全等。

结果

  • 通过Singleton.getInstance来获取Singleton类的唯一对象,方式简单,但增加了这个类的不透明性Singleton类的使用者必须知道这是一个单例类,跟以往通过new获取对象不同,这里要使用Singleton.getInstance来获取对象。

透明的单例模式

“透明”的单例类:用户从这个类中创建对象的时候,可以像使用其他任何普通类一样,下面的例子是使用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' ); 
console.log ( a === b ); // true

执行过程

  • 首先, CreateDiv是一个立即执行函数,先声明变量instance,再通过函数表达式定义函数,并在其原型上添加init方法,最后返回CreateDiv,当通过var a = new CreateDiv( 'sven1' );这行代码创建出构造函数的实例对象,将执行CreateDiv方法,因为instanceundefined,所以跳出if判断,并将sven1赋值给html,再执行init方法。由于构造函数本身没有init方法,就会去其原型上寻找,并成功的在页面中创建div节点,最终将this即实例a(结构如下)赋值给instance
    实例a
  • 接下来,当通过var b = new CreateDiv( 'sven2' );这行代码创建出构造函数的实例对象,由于 instance存在,则直接返回instance,结构也如上图所示。
  • 最终,a全等于b,输出true

结果

  • 为了把instance封装起来,使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
  • CreateDiv的构造函数负责了两件事情,第一是创建对象和执行初始化init方法,第二是保证只有一个对象
  • 同时,通过上面这个实例,假如我们要利用这个类在页面上创建千千万万的div,即要让这个类从单例类变成一个普通的可产生多个实例的类,那我们必须得改写CreateDiv构造函数,这也给我们带来不必要的烦恼。

用代理实现单例模式

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 ); 
}; 
// 接下来引入代理类 proxySingletonCreateDiv:
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' ); 
console.log ( a === b );
  • 通过引入代理类。在CreateDiv的构造函数中,把负责管理单例的代码移出去,使它成为一个普通的创建div的类。

JavaScript中的单例模式

单例模式的核心是确保只有一个实例,并提供全局访问
全局变量不是单例模式,但是在JS开发中,我们经常会把全局变量当成单例来使用,但是它存在容易造成命名空间污染的问题,以下是几种方式可以相对降低全局变量带来的命名污染。

  • 使用命名空间
    • 什么是命名空间?指某个函数名字只属于这个空间。相当于在函数名前面加了个前缀,用于标识该名字的所属空间。
    • 如何实现?
    var namespace1 = { 
       a: function(){ 
       	alert (1); 
       }, 
       b: function(){ 
       	 alert (2); 
       },
     }
    
    • 通过将a和b都定义成namespace1的属性,这样可以减少变量和全局作用域打交道的机会。
  • 使用闭包封装私有变量
    • 如何实现?
    • 私有变量__name 和 __age,它们被封装在闭包产生的作用域中,外部是访问不到这两变量,这就避免了对全局命令污染。
    var user = (function(){ 
       	var __name = 'sven', __age = 29; 
       	return { 
       		getUserInfo: function(){ 
      			return __name + '-' + __age; 
      		} 
      	} 
    })();
    

惰性单例

定义:在需要的时候才创建对象实例。

例如,当点击QQ头像时,会弹出一个登录浮窗,很明显浮窗在页面中烦恼总是唯一的。假如我们在页面加载完成的时候就创建好这个div浮窗,刚开始肯定是隐藏状态,当用户进行点击的时候,它才开始显示,代码如下:

<html> 
	<body> 
 		<button id="loginBtn">登录</button> 
 	</body> 
	<script> 
 		var loginLayer = (function(){ 
 			var div = document.createElement( 'div' ); 
 			div.innerHTML = '我是登录浮窗'; 
 			div.style.display = 'none'; 
 			document.body.appendChild( div ); 
 			return div; 
 		})(); 
 		document.getElementById( 'loginBtn' ).onclick = function(){ 
 			loginLayer.style.display = 'block'; 
 		}; 
	</script> 
</html>

但这样存在一个问题,有时候我们进入QQ,也许并不需要执行登陆操作,那一开始就创建好登陆浮窗,就会白白浪费一些DOM节点。因此可以修改代码使得用户点击登陆按钮的时候才开始创建该浮窗,代码如下:

<html> 
 	<body> 
 		<button id="loginBtn">登录</button> 
 	</body> 
	<script> 
 		var createLoginLayer = function(){ 
 			var 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,显然这样是不合理的,因此我们可以用一个变量来判断是否已经创建过登录浮窗,具体代码如下:

var createLoginLayer = (function(){ 
 	var div; 
 	return function(){ 
 		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'; 
};

参考文章:JavaScript设计模式与开发实践


版权声明:本文为weixin_43275787原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。