用JavaScript实现单例模式的最简单/最干净的方法是什么?


当前回答

我认为最干净的方法是:

var SingletonFactory = (function(){
    function SingletonClass() {
        //do stuff
    }
    var instance;
    return {
        getInstance: function(){
            if (instance == null) {
                instance = new SingletonClass();
                // Hide the constructor so the returned object can't be new'd...
                instance.constructor = null;
            }
            return instance;
        }
   };
})();

之后,可以调用函数as

var test = SingletonFactory.getInstance();

其他回答

我认为最简单的方法是声明一个简单的对象文字:

var myInstance = {
  method1: function () {
    // ...
  },
  method2: function () {
    // ...
  }
};

如果你想在你的单例实例上有私有成员,你可以这样做:

var myInstance = (function() {
  var privateVar = '';

  function privateMethod () {
    // ...
  }

  return { // public interface
    publicMethod1: function () {
      // All private members are accessible here
    },
    publicMethod2: function () {
    }
  };
})();

这被称为模块模式,它基本上允许您通过使用闭包来封装对象上的私有成员。

如果你想阻止对单例对象的修改,你可以使用ES5 object .freeze方法冻结它。

这将使对象不可变,防止对其结构和值进行任何修改。

如果你正在使用ES6,你可以很容易地使用ES模块来表示一个单例,你甚至可以通过在模块范围内声明变量来保持私有状态:

// my-singleton.js
const somePrivateState = []

function privateFn () {
  // ...
}

export default {
  method1() {
    // ...
  },
  method2() {
    // ...
  }
}

然后你可以简单地导入单例对象来使用它:

import myInstance from './my-singleton.js'
// ...

你可以用下面这个例子中的TypeScript装饰器来做:

class YourClass {

    @Singleton static singleton() {}

}

function Singleton(target, name, descriptor) {
    var instance;
    descriptor.value = () => {
        if(!instance) instance = new target;
        return instance;
    };
}

然后你像这样使用你的单例:

var myInstance = YourClass.singleton();

在撰写本文时,JavaScript引擎中还没有装饰器。你需要确保你的JavaScript运行时实际上启用了装饰器,或者使用像Babel和TypeScript这样的编译器。

还要注意的是,单例实例是“惰性”创建的,也就是说,只有当你第一次使用它时才会创建它。

下面是我实现单例模式的演练片段。这是我在面试过程中想到的,我觉得我应该在某个地方捕捉到这一点。

/*************************************************
 *     SINGLETON PATTERN IMPLEMENTATION          *
 *************************************************/

// Since there aren't any classes in JavaScript, every object
// is technically a singleton if you don't inherit from it
// or copy from it.
var single = {};


// Singleton Implementations
//
// Declaring as a global object...you are being judged!

var Logger = function() {
  // global_log is/will be defined in the GLOBAL scope here
  if(typeof global_log === 'undefined'){
    global_log = this;
  }
  return global_log;
};


// The below 'fix' solves the GLOABL variable problem, but
// the log_instance is publicly available and thus can be
// tampered with.
function Logger() {
  if(typeof Logger.log_instance === 'undefined') {
    Logger.log_instance = this;
  }

  return Logger.log_instance;
};


// The correct way to do it to give it a closure!

function logFactory() {
  var log_instance; // Private instance
  var _initLog = function() { // Private init method
    log_instance = 'initialized';
    console.log("logger initialized!")
  }
  return {
    getLog : function(){ // The 'privileged' method
      if(typeof log_instance === 'undefined') {
        _initLog();
      }
      return log_instance;
    }
  };
}


/***** TEST CODE ************************************************

// Using the Logger singleton
var logger = logFactory(); // Did I just give LogFactory a closure?

// Create an instance of the logger
var a = logger.getLog();

// Do some work
// Get another instance of the logger
var b = logger.getLog();

// Check if the two logger instances are same
console.log(a === b); // true
*******************************************************************/

同样可以在我的主旨页面上找到。

Christian C. Salvadó和zzzzBov的回答都给出了精彩的答案,但只是添加我自己的解释,基于我已经从PHP/Zend框架转向了沉重的Node.js开发,其中单例模式是常见的。

以下注释记录的代码基于以下需求:

函数对象的一个且只有一个实例可以被实例化 实例不是公开可用的,只能通过公共方法访问 构造函数不是公开可用的,只有在没有可用实例的情况下才可以实例化 构造函数的声明必须允许修改其原型链。这将允许构造函数从其他原型继承,并为实例提供“公共”方法

我的代码与zzzzBov的答案非常相似,除了我在构造函数中添加了一个原型链和更多的注释,这些注释应该有助于那些来自PHP或类似语言的人将传统的OOP转换为JavaScript的原型性质。它可能不是最简单的,但我相信它是最合适的。

// Declare 'Singleton' as the returned value of a self-executing anonymous function
var Singleton = (function () {
    "use strict";
    // 'instance' and 'constructor' should not be available in a "public" scope
    // here they are "private", thus available only within
    // the scope of the self-executing anonymous function
    var _instance=null;
    var _constructor = function (name) {
        this.name = name || 'default';
    }

    // Prototypes will be "public" methods available from the instance
    _constructor.prototype.getName = function () {
        return this.name;
    }

    // Using the module pattern, return a static object
    // which essentially is a list of "public static" methods
    return {
        // Because getInstance is defined within the same scope
        // it can access the "private" 'instance' and 'constructor' vars
        getInstance:function (name) {
            if (!_instance) {
                console.log('creating'); // This should only happen once
                _instance = new _constructor(name);
            }
            console.log('returning');
            return _instance;
        }
    }

})(); // Self execute

// Ensure 'instance' and 'constructor' are unavailable
// outside the scope in which they were defined
// thus making them "private" and not "public"
console.log(typeof _instance); // undefined
console.log(typeof _constructor); // undefined

// Assign instance to two different variables
var a = Singleton.getInstance('first');
var b = Singleton.getInstance('second'); // passing a name here does nothing because the single instance was already instantiated

// Ensure 'a' and 'b' are truly equal
console.log(a === b); // true

console.log(a.getName()); // "first"
console.log(b.getName()); // Also returns "first" because it's the same instance as 'a'

请注意,从技术上讲,自执行匿名函数本身就是一个单例函数,Christian C. Salvadó提供的代码很好地演示了这一点。这里唯一的问题是,当构造函数本身是匿名的时,不可能修改构造函数的原型链。

请记住,在JavaScript中,“公共”和“私有”的概念并不像在PHP或Java中那样适用。但是,通过利用JavaScript的函数作用域可用性规则,我们也达到了同样的效果。

简短的回答:

由于JavaScript的非阻塞特性,JavaScript中的单例在使用时非常难看。全局变量将在整个应用程序中为您提供一个实例,而不需要所有这些回调,模块模式将内部隐藏在接口后面。参见Christian C. Salvadó的回答。

但是,既然你想要一个单胞胎……

var singleton = function(initializer) {

  var state = 'initial';
  var instance;
  var queue = [];

  var instanceReady = function(createdInstance) {
    state = 'ready';
    instance = createdInstance;
    while (callback = queue.shift()) {
      callback(instance);
    }
  };

  return function(callback) {
    if (state === 'initial') {
      state = 'waiting';
      queue.push(callback);
      initializer(instanceReady);
    } else if (state === 'waiting') {
      queue.push(callback);
    } else {
      callback(instance);
    }
  };

};

用法:

var singletonInitializer = function(instanceReady) {
  var preparedObject = {property: 'value'};
  // Calling instanceReady notifies the singleton that the instance is ready to use
  instanceReady(preparedObject);
}
var s = singleton(singletonInitializer);

// Get the instance and use it
s(function(instance) {
  instance.doSomething();
});

解释:

单例在整个应用程序中提供了不止一个实例:它们的初始化延迟到第一次使用。当您处理初始化开销很大的对象时,这确实是一个大问题。昂贵通常意味着I/O,在JavaScript中I/O总是意味着回调。

不要相信那些给出instance = singleton.getInstance()这样接口的答案,它们都没有抓住重点。

如果它们没有在实例就绪时运行回调,那么当初始化器执行I/O时,它们将不起作用。

是的,回调看起来总是比函数调用更丑,函数调用会立即返回一个对象实例。但是同样:当你执行I/O时,回调是必须的。如果你不想做任何I/O,那么在程序启动时实例化就足够便宜了。

例1,简单的初始化器:

var simpleInitializer = function(instanceReady) {
  console.log("Initializer started");
  instanceReady({property: "initial value"});
}

var simple = singleton(simpleInitializer);

console.log("Tests started. Singleton instance should not be initalized yet.");

simple(function(inst) {
  console.log("Access 1");
  console.log("Current property value: " + inst.property);
  console.log("Let's reassign this property");
  inst.property = "new value";
});
simple(function(inst) {
  console.log("Access 2");
  console.log("Current property value: " + inst.property);
});

例2,初始化I/O:

在这个例子中,setTimeout模拟了一些昂贵的I/O操作。这说明了为什么JavaScript中的单例真的需要回调。

var heavyInitializer = function(instanceReady) {
  console.log("Initializer started");
  var onTimeout = function() {
    console.log("Initializer did his heavy work");
    instanceReady({property: "initial value"});
  };
  setTimeout(onTimeout, 500);
};

var heavy = singleton(heavyInitializer);

console.log("In this example we will be trying");
console.log("to access singleton twice before it finishes initialization.");

heavy(function(inst) {
  console.log("Access 1");
  console.log("Current property value: " + inst.property);
  console.log("Let's reassign this property");
  inst.property = "new value";
});

heavy(function(inst) {
  console.log("Access 2. You can see callbacks order is preserved.");
  console.log("Current property value: " + inst.property);
});

console.log("We made it to the end of the file. Instance is not ready yet.");