最近在研读曾探大佬的著作《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.instance为undefined。此时就通过new操作符实例化对象,这时候的this.instance就被赋值为Singleton {name: 'sven1', instance: null},即为a的值。- 接下来进行第二次调用,此时的this还是指向对象
Singleton,this.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方法,因为instance为undefined,所以跳出if判断,并将sven1赋值给html,再执行init方法。由于构造函数本身没有init方法,就会去其原型上寻找,并成功的在页面中创建div节点,最终将this即实例a(结构如下)赋值给instance。- 接下来,当通过
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版权协议,转载请附上原文出处链接和本声明。
