单例模式是指保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式是一种常用的模式,有一些对象往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。在javaScript开发中,单例模式的用途同样非常广泛。试想一下,单击登录按钮时,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建

标准单例

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

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 ); // true

  或者:

var Singleton = function( name ){
this.name = name;
};
Singleton.prototype.getName = function(){
alert ( this.name );
};
Singleton.getInstance = (
function(){
var instance = null;
return function( name ){
if ( !instance ){
instance = new Singleton( name );
}
})();
}
return instance;

  通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以往通过new XXX的方式来获取对象不同,这里偏要使用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');
alert(a === b); // true

  虽然现在完成了一个透明的单例类的编写,但它同样有一些缺点。为了把instance封装起来,使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服

  上面的代码中,CreateDiv构造函数实际上负责了两件事情。第一是创建对象和执行初始化init方法,第二是保证只有一个对象。这是一种不好的做法,至少这个构造函数看起来很奇怪。假设某天需要利用这个类,在页面中创建千千万万的div,即要让这个类从单例类变成一个普通的可产生多个实例的类,那必须得改写CreateDiv构造函数,把控制创建唯一对象的那一段去掉,这种修改会带来不必要的烦恼

代理实现单例

  现在通过引入代理类的方式,来解决上面提到的问题。依然使用上面的代码,首先在CreateDiv构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建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);
};
//引入代理类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');
alert(a === b);

  通过引入代理类的方式,同样完成了一个单例模式的编写,跟之前不同的是,现在把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果

惰性单例

  惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实际开发中非常有用

  下面继续以登录框的例子来说明

<button id="loginBtn">登录</button>
<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>

  这种方式有一个问题,如果根本不需要进行登录操作,登录浮窗一开始就被创建好,很有可能将白白浪费一些 DOM 节点

  现在改写一下代码,使用户点击登录按钮的时候才开始创建该浮窗

<button id="loginBtn">登录</button>
<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>

  虽然现在达到了惰性的目的,但失去了单例的效果。每次点击登录按钮时,都会创建一个新的登录浮窗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';
};

  上面的代码仍然存在如下问题:

  1、违反单一职责原则的,创建对象和管理单例的逻辑都放在 createLoginLayer对象内部

  2、如果下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍

    var createIframe= (function(){
var iframe;
return function(){
if ( !iframe){
iframe= document.createElement( 'iframe' );
iframe.style.display = 'none';
document.body.appendChild( iframe);
}
return iframe;
}
})();

 

通用惰性单例

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

var obj;
if ( !obj ){
obj = xxx;
}

  然后,把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数

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

  接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。之后再让getSingle返回一个新的函数,并且用一个变量result来保存fn的计算结果。result变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result已经被赋值,那么它将返回这个值

    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 loginLayer = createSingleLoginLayer();
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 = 'https://www.hao123.com';
};

  上面的例子中,创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能

  这种单例模式的用途远不止创建对象,比如通常渲染完页面中的一个列表之后,接下来要给这个列表绑定click事件,如果是通过ajax动态往列表里追加数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候被绑定一次,但不想判断当前是否是第一次渲染列表,如果借助于jQuery,通常选择给节点绑定one事件

    var bindEvent = function(){
$( 'div' ).one( 'click', function(){
alert ( 'click' );
});
};
var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};
render();
render();
render();

  如果利用getSingle函数,也能达到一样的效果

    var getSingle = function (fn) {
var result;
return function () {
return result || (result = fn.apply(this, arguments));
}
};
var bindEvent = getSingle(function(){
document.getElementById( 'div1' ).onclick = function(){
alert ( 'click' );
}
return true;
});
var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};
render();
render();
render();

  可以看到,render函数和bindEvent函数都分别执行了3次,但div实际上只被绑定了一个事件

最新文章

  1. css元素水平居中和垂直居中的方式
  2. sublime自动生成头部注释
  3. BZOJ4572: [Scoi2016]围棋
  4. iOS仿直播带有气泡动画的UIButton
  5. LeetCode4:Add Two Numbers
  6. PHP中的null合并运算符
  7. 算法与数据结构实验题 5.2 Missile
  8. POJ 1724 ROADS(二维SPFA)
  9. java常见类型的转化以及风险
  10. 基于SocketAsyncEventArgs的版本
  11. POJ 1808 Quadratic Residues(平方剩余相关)
  12. Android 系统内置App JNI
  13. jQuery 简单归纳总结
  14. document.readystate
  15. 利用Telnet来模拟Http请求 有GET和POST两种
  16. 软件下载网(包括MAC软件大全)
  17. openstack私有云布署实践【13.1 网络Neutron-compute节点配置(科兴环境)】
  18. SQL截取字符串
  19. 嵌入Python | 调用Python模块中有参数的函数
  20. asp.net 实现后台异步处理的方式

热门文章

  1. h5 上下左右前后居中
  2. thinkphp 高级模型
  3. 0818NOIP模拟测试赛后总结
  4. eigen矩阵操作练习
  5. springmvc jar包下载
  6. C++ Builder获取系统文件的路径
  7. DuiLib学习笔记3.颜色探究
  8. LintCode_13 字符串查找
  9. vue-cli 本地环境 API 代理设置和解决跨域
  10. 11-3-while