本文共 8068 字,大约阅读时间需要 26 分钟。
第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来获取对象。
- var a = Singleton.getInstance(‘sven1’);
- var b = Singleton.getInstance(‘sven2’);
- alert(a===b);
4.2 透明的单例模式
我们的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。在下面的例子中,我们将使用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;
- })();
上面的代码看上去会很难理解,在这段代码中,CreateDiv的构造函数实际上负责了两件事情。第一是创建对象和执行初始化的init方法,第二是保证只有一个对象。虽然我们目前我们还没学习到“单一职责原则”的概念,但可以明确的是,这是一种不好的做法,至少这个构造函数看起来好奇怪……,
假设我们某天需要利用这个类,在页面上创建多个div,即要让这个类从单例变成一人普通的可产生多个实例的类,那我们必须改写CreateDiv构造函数,把控制创建唯一那段去掉,这种修改会给我们带来不必要的烦恼。
4.3 用代理实现单例模式
现在我们通过引入代理方式,来解决上面提到的问题。
首先在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中。这样一来,CreateDiv就变成了一个普通类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。如下:
- <pre name="code" class="javascript">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);
4.4 javascript中的单例模式
前端提到的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。
js其实是一个无类语言,也正因为如此,生搬单例模式的概念并无意义。在中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个类呢(相当有同感)?
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例,但在javascript中,我们经常会把全局变量当成单例来使用如:
var a ={};
但这种方式比较糟糕的问题是,全名冲突。维护不容易啊
解决方法有如下两种:
1. 使用命名空间
适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
如下:
- namespace1={
- a:function(){
- alert(1);
- },
- b:function(){
- alert(2);
- }
- }
把a 和 b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。另外我们还可以动态地创建命名空间(Object-Oriented javascript)
- var MyApp = {};
- MyApp.namespace= function(name){
- var parts = name.split(‘.’);
- var current = MyApp;
- for(var i in parts){
- if(!current[parts[i]]){
- current[parts[i]] = {};
- }
- current = current[parets[i]];
- }
- }
- MyApp.namespace(‘event’);
- MyApp.namespace(‘dom.style’);
- consle.dir(MyApp);
-
- {
- event:{},
- dom:{
- style:{}
- }
- }
2. 全用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
- var user =(function(){
- var __name=’sven’,
- __age = 29;
- return {
- getUserInfo:function(){
- return __name+’-‘+__age;
- }
- }
- })();
4.5 惰性单例
需要才创建,这种技术在实际开发时非常有用,有用的程序超出我们的想象……,如我们在前面所讲的Singleton.getInstance的实现。但javascript中并不适用(因为它是基于类的创建方式生搬硬套感觉在实际应用中真真没啥用)。
- Singleton.getInstance=(function(){
- var instance =null;
- returnfunction(name){
- if(!instance){
- instance = new Singleton(name);
- }
- return instance;
- }
- })();
Demo Web QQ登录页面,当点击导航的QQ头像时,会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。
第一种解决方案在页面加载完成的时候便创建好这个div浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示
- <pre name="code" class="html"><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’).οnclick= 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。虽然我们可以在点击浮窗上的关闭按钮时把这个浮窗从页面中删除,但这样频繁地创建和删除节点明显是很不合理的,也是不必要的。
所以可以把 createLoginLayer改成单例模式
- <pre name="code" class="javascript">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’;
- }
4.6 通用的惰性单例
上一节中我们完成的一个可用的惰性单例,但是我们发现它还有如下一些问题。
o 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象的内部
o 如果我们下次需要创建页面中唯一的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 =xx ;
- }
现在我们将管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingleton函数内部,创建对象的方法fn被当成参数动态传入函数:
- 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’;
- return div;
- }
- var createSingleLoginLayer =getSingle(createLoginLayer);
- document.getElementById(‘loginBtn’).οnclick= 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’).οnclick= function(){
- var loginLayer =createSingleIframe();
- loginLayer.src =‘xxx’;
- }
这个例子挺有意思,单例模式的用途不止用于创建对象,比如我们通常渲染完页面中一个列表之后,接下来要给列表绑定click事件,如果是通过ajax动态往列表里追回数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是否是第一次渲染列表,如果我们是借助于,我们通常选择给节点绑定one事件
- var bindEvent = function(){
- $(‘div’).one(“click”,function(){
- alert(‘click’);
- });
- };
- var render = function(){
- console.log(‘开始渲染列表’);
- bindEvent();
- }
- render();
- render();
- render();
如果利用getSingle函数,也能达到一样的效果:
- 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实际上只被绑定了一个事件。
转载地址:http://agzxi.baihongyu.com/