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


当前回答

class Singelton {
    static #instance;

    #testValue;

    constructor() {
        if (Singelton.#instance instanceof Singelton) {
            return Singelton.#instance;
        }

        Singelton.#instance = this;
        return Singelton.#instance;
    }

    get testValue() {
        return this.#testValue;
    }

    set testValue(value) {
        this.#testValue = value;
    }
}

测试:

let x = new Singelton();
x.testValue = 123;

let y = new Singelton();

console.log({isSingelton: x === y, testValueFromY: y.testValue});

其他回答

简短的回答:

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

我反对我的回答,看看我的另一个。

通常,模块模式(参见Christian C. Salvadó的答案)不是单例模式就足够好了。然而,单例的一个特点是它的初始化会延迟到需要对象时。模块模式缺乏这个特性。

我的命题(CoffeeScript):

window.singleton = (initializer) ->
  instance = undefined
  () ->
    return instance unless instance is undefined
    instance = initializer()

在JavaScript中编译为:

window.singleton = function(initializer) {
    var instance;
    instance = void 0;
    return function() {
        if (instance !== void 0) {
            return instance;
        }
        return instance = initializer();
    };
};

然后我可以做以下事情:

window.iAmSingleton = singleton(function() {
    /* This function should create and initialize singleton. */
    alert("creating");
    return {property1: 'value1', property2: 'value2'};
});


alert(window.iAmSingleton().property2); // "creating" will pop up; then "value2" will pop up
alert(window.iAmSingleton().property2); // "value2" will pop up but "creating" will not
window.iAmSingleton().property2 = 'new value';
alert(window.iAmSingleton().property2); // "new value" will pop up

2015年ECMAScript (ES6):

class Singleton {
  constructor () {
    if (!Singleton.instance) {
      Singleton.instance = this
    }
    // Initialize object
    return Singleton.instance
  }
  // Properties & Methods
}

const instance = new Singleton()
Object.freeze(instance)

export default instance

下面是一个简单的例子来解释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();

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

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'
// ...