【学习笔记javascript设计模式与开发实践(单例模式)----4】

第4章单例模式

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

 单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器的window对象。在js开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录框,而这个浮窗是唯一的,无论单击多少次登录按钮,这个浮窗只会被创建一次。因此这个登录浮窗就适合用单例模式。

4.1 实现单便模式

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

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 XX的方式来获取对象不同,这里偏要使用Singleton.getInstance来获取对象。

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var a = Singleton.getInstance(‘sven1’);  
  2. var b = Singleton.getInstance(‘sven2’);  
  3. alert(a===b); //true;  

4.2 透明的单例模式

我们的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。在下面的例子中,我们将使用CreateDiv单例类,它的作用是负责在页面中创建唯一的div节点,如下:

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var CreateDiv = (function(){  
  2.  var instance;  
  3.  var CreateDiv =function(html){  
  4.    if(instance){  
  5.      return instance;  
  6.    }  
  7.    this.html = html;  
  8.    this.init();  
  9.    return instance = this;  
  10.  }  
  11.  CreateDiv .prototype.init= function(){  
  12.    var div = document.createElement(‘div’);  
  13.    div.innerHTML = this.html;  
  14.    document.body.appendChild(div);  
  15.  };  
  16.  return CreateDiv;  
  17. })();  

上面的代码看上去会很难理解,在这段代码中,CreateDiv的构造函数实际上负责了两件事情。第一是创建对象和执行初始化的init方法,第二是保证只有一个对象。虽然我们目前我们还没学习到“单一职责原则”的概念,但可以明确的是,这是一种不好的做法,至少这个构造函数看起来好奇怪……,

假设我们某天需要利用这个类,在页面上创建多个div,即要让这个类从单例变成一人普通的可产生多个实例的类,那我们必须改写CreateDiv构造函数,把控制创建唯一那段去掉,这种修改会给我们带来不必要的烦恼。

 

4.3 用代理实现单例模式

现在我们通过引入代理方式,来解决上面提到的问题。

首先在CreateDiv构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类:

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var CreateDiv = function(html){  
  2.  this.html = html;  
  3.  this.init();  
  4. }  
  5. CreateDiv.prototype.init = function(){  
  6.   var div = document.createElement(‘div’);  
  7.   div.innerHTML =this.html;  
  8.   document.body.appendChild(div);  
  9. }  

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

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <pre name="code" class="javascript">var ProxySingletonCreateDiv =(function(){  
  2.   var instance;  
  3.   return function(html){  
  4.    if(!instance){  
  5.       instance = new CreateDiv(html);  
  6.    }  
  7.    return instance;  
  8.  }  
  9. })();  
  10.    
  11. var a = new ProxySingletonCreateDiv(‘sven1’);  
  12. var b = new ProxySingletonCreateDiv(‘sven2’);  
  13. alert(a===b); //true  
 

4.4 javascript中的单例模式

前端提到的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在Java中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。

js其实是一个无类语言,也正因为如此,生搬单例模式的概念并无意义。在JavaScript中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个类呢(相当有同感)?

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

全局变量不是单例,但在javascript中,我们经常会把全局变量当成单例来使用如:

var a ={};

但这种方式比较糟糕的问题是,全名冲突。维护不容易啊

解决方法有如下两种:

1.    使用命名空间

适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。

如下:

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. namespace1={  
  2.    a:function(){  
  3.      alert(1);  
  4.    },  
  5.    b:function(){  
  6.      alert(2);  
  7.    }  
  8. }  

把a 和 b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。另外我们还可以动态地创建命名空间(Object-Oriented javascript)

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var MyApp = {};  
  2. MyApp.namespace= function(name){  
  3.    var parts = name.split(‘.’);  
  4.    var current = MyApp;  
  5.    for(var i in parts){  
  6.     if(!current[parts[i]]){  
  7.      current[parts[i]] = {};  
  8.     }  
  9.     current = current[parets[i]];  
  10.    }  
  11. }  
  12. MyApp.namespace(‘event’);  
  13. MyApp.namespace(‘dom.style’);  
  14. consle.dir(MyApp);  
  15. //结果  
  16. {  
  17.    event:{},  
  18.    dom:{  
  19.       style:{}  
  20.    }  
  21. }  

2.    全用闭包封装私有变量

这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var user =(function(){  
  2. var __name=’sven’,  
  3.    __age = 29;  
  4.   return {  
  5.       getUserInfo:function(){  
  6.          return __name+’-‘+__age;  
  7.       }  
  8.   }  
  9. })();  

4.5 惰性单例

需要才创建,这种技术在实际开发时非常有用,有用的程序超出我们的想象……,如我们在前面所讲的Singleton.getInstance的实现。但javascript中并不适用(因为它是基于类的创建方式生搬硬套感觉在实际应用中真真没啥用)。

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. Singleton.getInstance=(function(){  
  2. var instance =null;  
  3. returnfunction(name){  
  4.    if(!instance){  
  5.      instance = new Singleton(name);  
  6.   }  
  7.   return instance;  
  8. }  
  9. })();  

Demo Web QQ登录页面,当点击导航的QQ头像时,会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。

第一种解决方案在页面加载完成的时候便创建好这个div浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <pre name="code" class="html"><html>  
  2.   <body>  
  3.     <button id=”loginBtn”>登录</button>  
  4.   </body>  
  5.   <script>  
  6.    var loginLayer =(function(){  
  7.      var div = document.createElement(‘div’);  
  8.      div.innerHTML = “登录浮窗”;  
  9.      div.style.display = ‘none’;  
  10.      document.body.appendChild(div);  
  11.      return div;  
  12.    })();  
  13.    document.getElementById(‘loginBtn’).οnclick= function(){  
  14.      loginLayer.style.display = ‘block’;  
  15.    }  
  16.   </script>  
  17. </html>  

 

这种方式的缺点就是登录这个页面,不一定是启用登录QQ界面,如我们只是看看天气,根本不需要进行登录操作,因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些DOM节点。

那么我们将其改造一下,

[html]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <html>  
  2.   <body>  
  3.     <button id=”loginBtn”>登录</button>  
  4.   </body>  
  5.   <script>  
  6.   var createLoginLayer = function(){  
  7.     var div = document.createElement(‘div’);  
  8.     div.innerHTML = “登录浮窗”;  
  9.     div.style.display = ‘none’;  
  10.     document.body.appendChild(div);  
  11.     return div;  
  12.   };  
  13.   document.getElementById(‘loginBtn’).onclickfunction(){  
  14.     var loginLayer = createLoginLayer();  
  15.     loginLayer.style.display = ‘block’;  
  16.   }  
  17.   </script>  
  18. </html>  

在上例中虽然达到惰性的目的,但失去了单例的效果。当我们每次点击登录按钮的时候,都会创建一个新的登录浮窗div。虽然我们可以在点击浮窗上的关闭按钮时把这个浮窗从页面中删除,但这样频繁地创建和删除节点明显是很不合理的,也是不必要的。

所以可以把 createLoginLayer改成单例模式

[html]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <pre name="code" class="javascript">var createLoginLayer = (function(){  
  2.   var div;  
  3.   return function(){  
  4.     if(!div){  
  5.        div = document.createElement(‘div’);  
  6.        div.innerHTML = “登录浮窗”;  
  7.        div.style.display = ‘none’;  
  8.        document.body.appendChild(div);  
  9.     }  
  10.    return div;  
  11.   }  
  12. })();  
  13.   
  14. document.getElementById(‘loginBtn’).onclickfunction(){  
  15.     var loginLayer = createLoginLayer();  
  16.     loginLayer.style.display = ‘block’;  
  17. }  

 

4.6 通用的惰性单例

上一节中我们完成的一个可用的惰性单例,但是我们发现它还有如下一些问题。

o  这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象的内部

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

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var  createIframe = (function(){  
  2. var iframe;  
  3. return function(){  
  4.    if(!iframe){  
  5.       iframe =document.createElement(‘iframe’);  
  6.       iframe.style.display = ‘none’;  
  7.      document.body.appendChild(iframe);  
  8.    }  
  9.    return iframe;  
  10. }  
  11. })();  

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

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var obj;  
  2. if(!obj){  
  3.   obj =xx ; //bala…bala…  
  4. }  

现在我们将管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingleton函数内部,创建对象的方法fn被当成参数动态传入函数:

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var getSingle = function(fn){  
  2.   var result;  
  3.   return function(){  
  4.     return result || (result =fn.apply(this,arguments))  
  5.   }  
  6. }  
 

接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。

之后再让getSingle返回一个新的函数,并且一个变量result来保存fn的计算结果。result变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result已经被赋值,那么它将返回这个值。如下:

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var createLoginLayer = function(){  
  2.   var div = document.createElement(‘div’);  
  3.   div.innerHTML = ‘我是登录窗口’;  
  4.   div.style.display = ‘none’;  
  5.   return div;  
  6. }  
  7. var createSingleLoginLayer =getSingle(createLoginLayer);  
  8. document.getElementById(‘loginBtn’).οnclick= function(){  
  9.     var loginLayer = createSingleLoginLayer ();  
  10.     loginLayer.style.display = ‘block’;  
  11. }  

如我们还可以创建唯一一个iframe用于动态加载第三方页面

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var createSingleIframe =getSingle(function(){  
  2. var iframe =document.createElement(‘iframe’);  
  3.   document.body.appendChild(iframe);  
  4.   return iframe;  
  5. });  
  6.    
  7. document.getElementById(‘loginBtn’).οnclick= function(){  
  8.   var loginLayer =createSingleIframe();  
  9.   loginLayer.src =‘xxx’;  
  10. }  

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

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var bindEvent = function(){  
  2.  $(‘div’).one(“click”,function(){  
  3.    alert(‘click’);  
  4.  });  
  5. };  
  6. var render = function(){  
  7.   console.log(‘开始渲染列表’);  
  8.   bindEvent();  
  9. }  
  10. render();  
  11. render();  
  12. render(); //<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>  

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

[javascript]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. var bindEvent = getSingle(function(){  
  2.  document.getElementById(‘div1’).onclick = function(){  
  3.    alert(‘click’);  
  4.   }  
  5.  return true;  
  6. });  
  7. var render = function(){  
  8.   console.log(‘开始渲染列表’);  
  9.   bindEvent();  
  10. }  
  11. render();  
  12. render();  
  13. render();  

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






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值