单例模式
简介
什么是单例模式呢?
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
看完单例模式的定义可能还还是比较模糊,所以接下来咱们一起来揭开单例模式的'神秘面纱'吧~
实现单例模式
我们先从最简单的单例模式入手,根据单例模式的定义可知,要保证一个类仅有一个实例的话我们会需要进行判断,如果该类已经创建过实例了我们就不再进行创建而是直接返回之前创建的实例,如果没有创建过则先创建实例再返回即可。所以实现一个标准的单例模式并不复杂,无非就是用一个变量来标志当前是否已经为某个类创建过实例。
标准的单例模式
var Singleton = function( name ){
this.name = name;
}
Singleton.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( 'hello' );
var b = Singleton.getInstance( 'world' );
alert( a === b ); //true
我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单但是存在一个问题,就是增加了这个类的'不透明性',使用者必须知道这是一个单例类,和以往通过new XXX的方式来获取对象不同,这里需要使用Singleton.getInstance来获取对象。
透明的单例模式
接下来我们的目标就是实现一个'透明'的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。下面我们将使用creatSpan单例类,它的作用是在页面中创建唯一的span节点。
creatSpan
var CreatSpan = (function(){
var instance;
// 构造函数
var CreateSpan = function ( html ){
// 保证只有一个对象
if( instance ){
return instance;
}
this.html = html; // 创建对象
this.init(); // 初始化
return instance = this;
};
CreateSpan.prototype.init = function(){
var span = document.createElement( 'span' );
span.innerHTML = this.html;
document.body.appendChild(span);
}
return CreateSpan;
})();
var a = new CreateSpan('span1');
var b = new CreateSpan('span2');
alert(a === b); // true
creatSpan虽然是一个透明的单例类,但它同样也有一个缺点,为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,降低了可读性。
用代理实现单例模式
现在我们引入代理类的方式,来解决上面的问题。
js
var CreatSpan = function(html){
this.html = html;
this.init();
}
CreatSpan.prototype.init = funciton(){
var span = document.createElement( 'span' );
span.innerHTML = this.html;
document.body.appendChild(span);
}
var ProxySingletonCreatSpan = (function(){
var instance;
return function(html){
if( !instance ){
instance = new CreatSpan( html );
}
return instance;
}
})()
通过引入代理类的方式,把负责管理单例的逻辑移到了代理类ProxySingletonCreatSpan中,这样一来,createSpan就变成了一个普通的类,和ProxySingletonCreatSpan组合起来就能达到单例模式的效果。
JavaScript中的单例模式
单例模式的核心是确保只有一个实例,并提供全局访问
。在JavaScript开发中,我们经常会把全局变量当成单例来使用,但全局变量不是单例模式
。
js
var a = {}
当用这种方式创建对象a时,对象a确实是独一无二的。如果a变量被声明在全局作用域下,则满足了单例模式的两个条件,但不能保证其只会实例化一个对象。
惰性单例
惰性单例指的是在需要的时候才创建对象实例。实际上在实现单例模式时就使用过这种技术,instance实例对象总是在我们调用Singleton.getInstance的时候才创建,而不是页面加载好的时候创建。 下面我们将以微信的登录浮窗为例,介绍与全局变量结合实现惰性的单例。当点击微信图标时,会弹出一个登录浮窗,很明显这个浮窗是唯一的,不可能出现同时存在两个登录窗口的情况。
第一种方案是在页面加载完成时创建好div浮窗,这个浮窗一开始是隐藏状态,当用户点击登录按钮时才显示;
js
<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>
这种方式有一个问题,因为登录浮窗总是一开始就被创建好,那么很有可能就会浪费一些DOM节点。现在改写一下代码,当用户点击登录按钮时才开始创建登录浮窗:
js
<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。虽然我们可以在点击浮窗上的关闭按钮时把这个浮窗从页面中删除,但这样频繁地创建和删除节点明显是不合理也是不必要的。
js
<html>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
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';
}
</script>
</html>
通用的惰性单例
上一节虽然实现了惰性单例,但还是存在一些问题:
- 违反了单一职责原则,创建对象和管理单例的逻辑都放在createLoginLayer对象内部;
- 如果下次需要创建iframe或者script标签就只能把createLoginLayer函数复制一遍;
所以我们需要把不变的部分隔离出来:
js
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( 'loginBtn' ).onclick = function(){
var singleLoginLayer = createSingleLoginLayer();
singleLoginLayer.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 singleIframe = createSingleIframe();
singleIframe.src = 'http://baidu.com';
}
在这个例子中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化且互不影响。
这种单例模式的用途远不止创建对象,比如在渲染完页面中的一个列表之后需要给这个列表绑定click事件,click事件实际上只需要在第一次渲染列表的时候被绑定一次,利用getSingle实现如下:
js
var bindEvent = getSingle(function(){
document.getElementById('demo').addEventListener = function(){
alert( 'click' );
}
return true;
});
var render = function(){
consolr.log('start');
bindEvent();
};
rendre();
rendre();
rendre();
可以看到render函数和bindEvent函数都分别执行了3次,但div实际上只被绑定了一个事件;
总结
单例模式是一种简单但非常实用的模式,特别是惰性单例,在合适的时候才创建对象,并且只创建唯一的一个,因为只允许创建一个对象,因此节省内存,加快对象访问速度,但与此同时也降低了灵活性,如果同一类型的对象总是要在不同的用例场景发生变化,就会引起数据的错误,因为不能保存彼此的状态,所以单例不适用于变化的对象。