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

单例模式

单例模式的定义是:

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

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器的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);
};

// 引入代理类 proxySingletonCreateDiv
var 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设计模式与开发实践》第四章 笔记。

    原文作者:设计模式
    原文地址: https://segmentfault.com/a/1190000003999776
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞