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


当前回答

这个知识是基于我正在学习Java,虽然Java和Javascript是不同的,但单例的概念和Java如何做到这一点是一样的。在我看来,JS的类样式本身是干净的,而不是var初始化。

class Singleton {
    // use hashtag which entails that the variable can only be accessed from self scope
    static #instance = null;
    static getInstance() {
        if (this.#instance === null) this.#instance = new Singleton();
        return this.#instance;
    }

    // some class property
    hello = 'world';

    // or initialize the variable in the constructor, depend on your preference
    constructor() {
        // this.hello = 'world';
    }

    /* you can also add parameters on the constructor & getInstance
     * e.g. 
     * static getInstance(param1, param2) {...new Singleton(param1, param2)}
     * constructor(param1, param2) {...}
     */

}




// this is the same code for java and normal way for singleton for class
// just use static so you can get instance


// testing the singleton
var s1,s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();

// you cannot access the property, immediately
if (Singleton.hello === undefined) console.log('getInstance so you can access this');

console.log(s1.hello);
// result: "world"

console.log(s2.hello);
// result: "world"


// set the value of Singleton object
s2.hello = "hi";
    console.log(s1.hello);
    // result: "hi"

    console.log(s2.hello);
    // result: "hi"
// this is just an evidence which means that they are the same even in property level

if (s1 === s2) console.log("S1 & S2 is the same object");
// result: "S1 & S2 is the same object"

// don't use something like `var s1 = new Singleton();`
// this will defeat your purpose of just (1 object), one instance of class

其他回答

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的函数作用域可用性规则,我们也达到了同样的效果。

let MySingleton = (function () {
  var _instance
  function init() {
    if(!_instance) {
      _instance = { $knew: 1 }
    }
    return _instance
  }
  let publicAPIs = {
    getInstance: function() {
      return init()
    }
  }
  // this prevents customize the MySingleton, like MySingleton.x = 1
  Object.freeze(publicAPIs) 
  // this prevents customize the MySingleton.getInstance(), like MySingleton.getInstance().x = 1
  Object.freeze(publicAPIs.getInstance())
  return publicAPIs
})();

下面是一个简单的例子来解释JavaScript中的单例模式。

var Singleton = (function() {
    var instance;
    var init = function() {
        return {
            display:function() {
                alert("This is a singleton pattern demo");
            }
        };
    };
    return {
        getInstance:function(){
            if(!instance){
                alert("Singleton check");
                instance = init();
            }
            return instance;
        }
    };
})();

// In this call first display alert("Singleton check")
// and then alert("This is a singleton pattern demo");
// It means one object is created

var inst = Singleton.getInstance();
inst.display();

// In this call only display alert("This is a singleton pattern demo")
// it means second time new object is not created,
// it uses the already created object

var inst1 = Singleton.getInstance();
inst1.display();

简短的回答:

由于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.");

您可以在每次新的执行中返回相同的实例-

function Singleton() {
    // lazy 
    if (Singleton.prototype.myInstance == undefined) {
        Singleton.prototype.myInstance = { description: "I am the instance"};
    }
    return Singleton.prototype.myInstance;
}

a = new Singleton();
b = new Singleton();
console.log(a); // { description: "I am the instance"};
console.log(b); // { description: "I am the instance"};
console.log(a==b); // true