原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#singletonpatternjavascript
单例模式命名源自它将类的实例限制为单个对象。通常,单例模式通过创建一个类来声明,这个类会包含一个方法,它会在类实例不存在的时候创建一个新实例。在类实例已经存在的情况,它只是简单的返回那个对象的引用。
单例区别于静态类(或者对象)的地方在于我们可以延后它们的初始化,通常是因为它需要的一些信息是在初始化时还无法获取。它们不为先前不知道它们的引用的代码提供一种容易检索它们的方式。这是因为单例返回的既不是对象也不是类,它是一个结构。考虑一下闭包变量是如何被“封闭”的 - 函数作用域提供了闭包来 “封闭”。
在 JavaScript 中,单例作为共享资源的命名空间,将实现代码与全局命名空间隔离,以便为函数提供单一的访问点。
我们可以按照以下方式来声明一个单例:
var mySingleton = (function () {
// 实例存储了一个指向单例的引用
var instance;
function init() {
// 单例
// 私有的函数和变量
function privateMethod(){
console.log( "I am private" );
}
var privateVariable = "Im also private";
var privateRandomNumber = Math.random();
return {
// 公共方法和变量
publicMethod: function () {
console.log( "The public can see me!" );
},
publicProperty: "I am also public",
getRandomNumber: function() {
return privateRandomNumber;
}
};
};
return {
// 如果实例存在就返回它,不存在就创建一个
getInstance: function () {
if ( !instance ) {
instance = init();
}
return instance;
}
};
})();
var myBadSingleton = (function () {
// 实例存储了一个指向单例的引用
var instance;
function init() {
// 单例
var privateRandomNumber = Math.random();
return {
getRandomNumber: function() {
return privateRandomNumber;
}
};
};
return {
// 总是创建一个新的实例
getInstance: function () {
instance = init();
return instance;
}
};
})();
// 用法:
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
// 注意:因为我们使用随机数字,理论上是会出现重复数字的几率,尽管概率比较低。
// 否则,上面的例子仍然是有效的。
形成单例的原因是因为我们在全局作用域访问实例不是通过直接调用 new MySingleton()
(至少在静态语言中是),通常是通过 MySingleton.getInstance()
。但是,这在 JavaScript 是可以的。
在 GoF 书中,单例模式的适用情况是:
- 类只能有一个实例,并且提供一个通用的访问点给调用方(如
getInstance()
)。 - 当唯一的实例需要被子类拓展时,调用方应该能够使用已拓展的实例而无需修改它们的代码。
第二点是因为我们可能需要这样的代码:
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 () {
// 选项:一个包含单例配置项的对象
// 例如:var options = { name: "test", pointX: 5};
function Singleton( options ) {
// 接收传入的 options 或者赋值为空对象
options = options || {};
// 设置单例的属性
this.name = "SingletonTester";
this.pointX = options.pointX || 6;
this.pointY = options.pointY || 10;
}
// 实例的载体
var instance;
// 模拟的静态变量和方法
var _static = {
name: "SingletonTester",
// 或者实例的方法. 它会返回一个实例的实例
getInstance: function( options ) {
if( instance === undefined ) {
instance = new Singleton( options );
}
return instance;
}
};
return _static;
})();
var singletonTest = SingletonTester.getInstance({
pointX: 5
});
// 输出 pointX 来验证它是否正确
// 输出: 5
console.log( singletonTest.pointX );
虽然单例很有用,但通常当我们发现在 JavaScript 中需要用到它时,我们可能会需要重新评估一些我们的设计。
它们通常预示着系统中的模块要么是紧耦合,要么逻辑过度分散在代码库中的多个部分。由于隐性依赖关系、无法创建多实例和难以阻塞依赖关系等问题,使得单例难以测试。
Miller Medeiros 此前曾推荐阅读这篇关于单例的优秀 文章 以及这篇关于单例各种问题及评论的 文章 ,讨论单例为何会增加紧耦合。我很乐意再次做推荐一下,因为每篇文章都提出了很多关于这个模式没有价值的观点。