JavaScript单例模式

404 阅读5分钟

创作声明:如需转载,请注明出处

本文首发于知乎专栏

前言

最近在学习 JavaScript 的设计模式,在通过人民邮电出版社出版的《JavaScript设计模式》([美] Addy Osmani 著)学习的过程中,发现书中所译不仅没有体现原文的精华,而且还经常使我迷惑。

对此,我把视线投入到了网络,找到了 W3Cschool 的译本,然而也发现其中有不少地方翻译的让人很迷惑,这迫使我不得不去看原著,只是没想到原著也如此难找,最终在某个犄角旮瘩找到了。

Learning JavaScript Design Patterns

下文是由我逐字逐句研读原著过程中,根据自己的理解记录下的翻译。当然在研读的过程中也参考了 W3Cschool 的译本和人民邮电出版社出版的《JavaScript设计模式》。若有翻译不到之处,希望大家可以在评论中指正。

正文

单例模式之所以这么叫,是因为它限制一个类只能有一个实例化对象。经典的实现方式是,创建一个类,这个类包含一个方法,这个方法在没有对象存在的情况下,将会创建一个新的实例对象。如果对象存在,这个方法只是返回这个对象的引用。

单例不同于静态类(或对象),因为我们可以推迟他们的初始化,这通常是因为他们需要一些在初始化的期间可能无法获得的信息。它们没有提供一种方法,使不知道先前对它们的引用的代码可以轻松地检索它们。这是因为单体返回的既不是对象也不是“类”,而是一个结构。可以类比闭包中的变量不是闭包-提供闭包的函数作用域是闭包。

在 JavaScript 中,单例作为一个共享的资源命名空间,它将实现代码与全局命名空间隔离开来,从而为函数提供一个单一的访问点。

我们能像这样实现一个单例:

var mySingleton = (function () {
 
  // Instance stores a reference to the Singleton
  var instance;
 
  function init() {
 
    // Singleton
 
    // Private methods and variables
    function privateMethod(){
        console.log( "I am private" );
    }
 
    var privateVariable = "Im also private";
 
    var privateRandomNumber = Math.random();
 
    return {
 
      // Public methods and variables
      publicMethod: function () {
        console.log( "The public can see me!" );
      },
 
      publicProperty: "I am also public",
 
      getRandomNumber: function() {
        return privateRandomNumber;
      }
 
    };
 
  };
 
  return {
 
    // Get the Singleton instance if one exists
    // or create one if it doesn't
    getInstance: function () {
 
      if ( !instance ) {
        instance = init();
      }
 
      return instance;
    }
 
  };
 
})();
 
var myBadSingleton = (function () {
 
  // Instance stores a reference to the Singleton
  var instance;
 
  function init() {
 
    // Singleton
 
    var privateRandomNumber = Math.random();
 
    return {
 
      getRandomNumber: function() {
        return privateRandomNumber;
      }
 
    };
 
  };
 
  return {
 
    // Always create a new Singleton instance
    getInstance: function () {
 
      instance = init();
 
      return instance;
    }
 
  };
 
})();
 
 
// Usage:
 
var singleA = mySingleton.getInstance();
var singleB = mySingleton.getInstance();
console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true
 
var badSingleA = myBadSingleton.getInstance();
var badSingleB = myBadSingleton.getInstance();
console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true
 
// Note: as we are working with random numbers, there is a
// mathematical possibility both numbers will be the same,
// however unlikely. The above example should otherwise still
// be valid.

创建一个全局访问的单例实例 (通常通过 MySingleton.getInstance()) 因为我们不能(至少在静态语言中) 直接调用 new MySingleton() 创建实例. 这在 JavaScript 语言中是可能的。

在 GoF 书里面,单例模式的应用描述如下:

  • 每个类只有一个实例,这个实例必须通过一个广为人知的接口,来被客户访问。
  • 子类如果要扩展这个唯一的实例,客户可以不用修改代码就能使用这个扩展后的实例。

关于第二点,可以参考如下的实例,我们需要这样编码:

mySingleton.getInstance = function(){
  if ( this._instance == null ) {
    if ( isFoo() ) {
       this._instance = new FooSingleton();
    } else {
       this._instance = new BasicSingleton();
    }
  }
  return this._instance;
};

这里,getInstance 变得有点像工厂方法,我们不需要更新每个访问单例的代码。上面的 FooSingleton 将是 BasicSingleton 的一个子类,并实现相同的接口。

为什么对于单例模式来讲,延迟执行这么重要?

在 C++ 代码中,单例模式将不可预知的动态初始化顺序问题隔离掉,将控制权返回给程序员。

重要的是要注意类(对象)的静态实例和单例实例之间的区别:虽然单例实例可以实现为静态实例,但是它也可以延迟构建,在实际需要之前不需要资源和内存。

如果我们有一个可以直接初始化的静态对象,我们需要确保代码总是以相同的顺序执行(例如,如果 objCar 在初始化过程中需要 objWheel),并且当你有很多源文件的时候,这种方式没有可扩展性。

单例模式和静态对象都很有用,但是不能滥用-同样的我们也不能滥用其它模式。

在实践中,当一个对象需要和另外的对象进行跨系统协作的时候,单例模式很有用。下面是一个单例模式在这种情况下使用的例子:

var SingletonTester = (function () {
 
  // options: an object containing configuration options for the singleton
  // e.g var options = { name: "test", pointX: 5};
  function Singleton( options ) {
 
    // set options to the options supplied
    // or an empty object if none are provided
    options = options || {};
 
    // set some properties for our singleton
    this.name = "SingletonTester";
 
    this.pointX = options.pointX || 6;
 
    this.pointY = options.pointY || 10;
 
  }
 
  // our instance holder
  var instance;
 
  // an emulation of static variables and methods
  var _static = {
 
    name: "SingletonTester",
 
    // Method for getting an instance. It returns
    // a singleton instance of a singleton object
    getInstance: function( options ) {
      if( instance === undefined ) {
        instance = new Singleton( options );
      }
 
      return instance;
 
    }
  };
 
  return _static;
 
})();
 
var singletonTest = SingletonTester.getInstance({
  pointX: 5
});
 
// Log the output of pointX just to verify it is correct
// Outputs: 5
console.log( singletonTest.pointX );

尽管单例模式有着合理的使用需求,但当我们发现自己在 JavaScript 中需要使用此模式时,这就意味着我们可能需要重新评估我们的设计。

它们通常表示系统中的模块要么紧密耦合,要么逻辑过度分散在代码库中的多个部分。由于隐藏的依赖关系、创建多个实例的困难、阻塞依赖关系的困难等问题,单例可能更难测试。

要想进一步了解关于单例的信息,可以读读 Miller Medeiros 推荐的这篇非常棒的关于单例模式以及单例模式各种各样问题的文章,也可以看看这篇文章的评论,这些评论讨论了单例模式是怎样增加了模块间的紧耦合。我很乐意去支持这些推荐,因为这两篇文章提出了很多关于单例模式重要的观点,而这些观点是很值得重视的。