设计模式实践之单例模式

最近因为翻译Vue-ssr文档的原因,其中涉及到了在Node服务端渲染时,不能采取单例模式,需要暴露一个工厂函数使得对于每一个请求都返回一个全新的vue实例,否则会因为状态共享的特性,而引起跨请求的状态污染。这篇文章就说说非常常见的单例模式。

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

在Javascript中单例模式的用途是很广泛的,很多时候一个对象我们往往只需要一个,比如浏览器中的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('a');
var b = Singleton.getInstance('b');

console.log(a === b); // true

可以看到这个方式有一个问题,就是我们没有直接使用常用的new关键字来获取实例,而是使用getInstance方法,那么对于一个我们无法确定是不是单例模式的构造函数呢,这就增加了这个类的不透明性。

现在我们假设需要创建一个页面中唯一的div节点,代码如下:

var OnlyDiv = (function() {
  var instance;
  
  var OnlyDiv = function(html) {
    if (instance) {
      return instance;
    }
    this.html = html;
    this.init();
    return instance = this;
  }
  
  OnlyDiv.prototype.init = function() {
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
  return OnlyDiv;
})();

var a = new OnlyDiv('a');
var b = new OnlyDiv('b');

console.log(a === b) // true

这里我们使用IIFE(Immediately-Invoked Function Expression)和闭包来封装,并且匿名函数返回了真正的构造函数,可以直接使用new关键字来实例化,但是稍微增加了程序的复杂度,也有点难以阅读。另外整个函数把构造div和确保只有单例这两处逻辑耦合在一起,以后要修改的话也有点麻烦,继续优化的话我们可以把这两部分分开来:

var OnlyDiv = function(html) {
  this.html = html;
  this.init();
}

OnlyDiv.prototype.init = function() {
  var div = document.createElement('div');
  div.innerHTML = this.html;
  document.body.appendChild(div);
}

// 引入一个代理
var ProxyOnlyDiv = (function() {
  var instance;
  return function(html) {
    if (!instance) {
      instance = new OnlyDiv(html);
    }
    return instance;
  }
})();

var a = new ProxyOnlyDiv('a');
var b = new ProxyOnlyDiv('b');

console.log(a === b) // true;

这样以来,我们把两种不同的逻辑完全分开来,两者组合就达到了单例模式的效果。以上,其实都是传统的面向对象的实现,以类为中心,但是对于js来说其实是没有类这一概念的,ES6的class其实也只是语法糖,既然我们只需要一个唯一的对象,全局对象其实就经常被我们当成单例来使用:

var a = {};

当然全局对象由于污染命名的问题并不鼓励使用,我们可以适当使用命名空间或者闭包来封装私有变量:

// 使用命名空间减少全局变量的数目
var namespace = {
  a: function() {
    console.log('a');
  },
  b: function() {
    console.log('b');
  },
}

// 把变量封装在闭包内部,只暴露固定接口,这样也减少了对全局的污染
var user = (function {
  var _name = 'trump';
  var _age = 70;
  
  return {
    getUserInfo: function() {
      console.log(_name + ' is ' + _age + ' years old.');
    }
  }
})();

user.getUserInfo(); // trump is 70 years old.