当前位置

网站首页> 程序设计 > 开源项目 > 程序开发 > 浏览文章

JavaScript设计模式与开发实践 | 04 - 单例模式

作者:小梦 来源: 网络 时间: 2024-06-24 阅读:

单例模式

单例模式的定义是:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器的window对象等。例如,当我们点击登录按钮时,页面会弹出一个登录悬浮窗,而这个登录悬浮窗是唯一的,无论点击多少次登录按钮,这个悬浮窗只会被创建一次,这时,这个悬浮窗就适合用单例模式来创建。

实现单例模式

实现一个标准的单例模式,一般是用一个变量来标志当前是否已经为某个类创建过对象,若是,则在下一次获取该类的实例时,直接返回之前创建的对象。

不透明的单例模式

var Singleton = function(name){  this.name = name;  this.instance = null;}Singleton.prototype.getName = function(){  console.log(this.name);};Singleton.getInstance = function(name){  if(! this.instance){      this.instance  = new Singleton(name);  }  return this.instance;};var a = Singleton.getInstance('sin1');var b = Singleton.getInstance('sin2');console.log(a === b);  // 输出:true

我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式想对简单,但有一个问题,就是增加了类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以往通过new xxx来获取对象的方式不同,这里只能使用Singleton.getInstance来获取对象。

透明的单例模式

现在我们通过一段代码来实现一个透明的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。

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('sin1');var b = new createDiv('sin2');console.log(a === b);  // 输出:true

为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。

观察Singleton构造函数的代码,该构造函数实际上负责了两件事情:第一是创建对象和执行初始化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);};// 引入代理类 proxySingletonCreateDivvar proxySingletonCreateDiv = (function(){  var instance;  return function(html){      if(!instance){        instance = new createDiv(html);      }      return instance;  }})();var a = new proxySingletonCreateDiv('sin1');var b = new proxySingletonCreateDiv('sin2');

我们把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中。这样一来,createDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来就可以达到单例模式的效果;如果单独使用,就作为一个普通的类,能产生多个实例对象。

JavaScript中的单例模式

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

但JavaScript是一门无类语言,生搬单例模式的概念并无意义。在JavaScript中创建对象非常简单,直接声明即可。既然这样,我们就没有必要为它先创建一个类。

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

全局变量不是单例模式,但在JavaScript开发中,我们经常会把全局变量当成单例模式来使用,例如var a = {};

当用这种方式创建对象a时,对象a确实独一无二。如果变量a被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量自然能全局访问。这样就满足了单例模式的两个条件。

但是,全局变量存在一些问题:

  • 容易造成命名空间污染;

  • 在大型项目中,如果不加以限制和管理,程序中可能存在很多这样的变量;

  • JavaScript中的变量很容易被不小心覆盖。

因此,在使用全局变量时,我们要尽力降低它的污染,通过以下方式:

1.使用命名空间
适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
最简单的方法依然是用对象字面量的方式:

var namespace1 = {  a: function(){      alert(1);  },  b: function(){      alert(2);  }};

把a和b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。‘

另外,可以动态地创建命名空间,如:

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[parts[i]];  }};myApp.namespace('event');myApp.namespace('dom.style');

上述代码等价于:

var myApp = {  event:{},  dom:{    style:{}  }};

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

var user = (function(){  var __name = 'sin1';  var __age = 29;  return {      getUserInfo: function(){          return __name + '-' + __age;      }  }})();

我们用下划线来约定私有变量__name和__age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命名污染。

惰性单例

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

我们在开头写的Singleton类就用过这种技术,instance实例对象总是在我们调用Singleton.getInstance的时候才被创建,而不是在页面加载好的时候就创建。

实现惰性单例

假设,在一个提供登录功能(点击登录按钮弹出一个登录悬浮窗)的web页面中,可能用户在访问过程中,根本不需要进行登录操作,只需要浏览某些内容。所以,没有必要在页面加载好之后就马上创建登录悬浮窗,只需要当用户点击登录按钮的时候才开始创建登录悬浮窗,实现代码如下:

<!DOCTYPE html><html><head>  <title>惰性单例</title></head><body>  <button id = "loginBtn">登录</button></body><script type="text/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';  };</script></html>

但这段代码还是存在一些问题的:

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

  • 如果我们下次需要创建页面中唯一的iframe,或者script标签,必须得如法炮制,把createLoginLayer函数几乎照抄一遍。

通用的惰性单例

为了解决上面的问题,我们可以实现一段通用的惰性单例代码:

<!DOCTYPE html><html><head>  <title>惰性单例</title></head><body>  <button id = "loginBtn">登录</button></body><script type="text/javascript">  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 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 = 'http://baidu.com';  };</script></html>

上面的代码,

  • 把管理单例的逻辑抽象了出来:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象;

  • 把如何管理单例的逻辑封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数;

  • 将创建登录悬浮窗的方法传入getSingle,还能传入createIframe,createScript;

  • getSingle函数返回一个新的函数,并且用一个变量result来保存fn的计算结果,result变量在闭包中,永远不会被销毁,所以在将来的请求中,如果result已经被赋值,那么它将返回这个值。

单例模式的用途不止在于创建对象,比如我们通常渲染完页面中的一个列表后,就要给这个列表绑定click事件,如果通过ajax动态往列表里追加数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候就被绑定一次。

<!DOCTYPE html><html><head>  <title>惰性单例</title></head><body>  <button id = "renderBtn">渲染列表</button></body><script type="text/javascript">  var getSingle = function(fn){      var result;      return function(){        return result || (result = fn.apply(this, arguments));      }  };  var bindEvent = getSingle(function(){      console.log('绑定click事件');      document.getElementById('renderBtn').onclick = function(){          alert('click');      }      return true;  });  var render = function(){      console.log('开始渲染');      bindEvent();  }  render();  render();  render();  // 最终输出结果:  // 开始渲染  // 绑定click事件  // 开始渲染  // 开始渲染</script></html>

PS:本节内容为《JavaScript设计模式与开发实践》第四章 笔记。

热点阅读

网友最爱