另一种实现方式的传送门:【TS】另一种实现typescript单例模式的方式(支持代码提示,禁止二次实例化) - 简书 (jianshu.com)
先贴出我的代码,后面再讲讲为什么要这么做:
class Singleton {
// 实例
private static _instance: Singleton;
// 是否是通过getInstance实例化
private static _instantiateByGetInstance: boolean = false;
/**
* 获取实例
*/
public static getInstance<T extends Singleton>(this: (new () => T) | typeof Singleton): T {
const _class = this as typeof Singleton;
if (!_class._instance) {
_class._instantiateByGetInstance = true;
_class._instance = new _class();
_class._instantiateByGetInstance = false;
}
return _class._instance as T;
}
/**
* 构造函数
* @protected
*/
protected constructor() {
if (!(this.constructor as typeof Singleton)._instantiateByGetInstance) {
throw new Error("Singleton class can't be instantiated more than once.");
}
}
}
我在网上搜索了一遍,大多数文章里的ts单例模式或多或少是不够完美的,我的单例模式有以下几个优点:
- 子类可以直接继承此
Singleton
类,无需其他多余代码,比如
class TestClass extends Singleton {
}
- 支持IDE代码提示
class TestClass extends Singleton {
testFunc() {
}
}
-
禁止使用new关键字实例化
接下来讲讲为什么要这么写
-
getInstance
有泛型参数T,它需要继承Singleton
,使用的时候不需要添加泛型参数,当然加上也没问题,比如:TestClass.getInstance<TestClass>()
。 - ts函数参数列表中的第一项可以为
this
。这个this
只是用于手动告诉ts编译器它的类型,实际并不需要传入。 -
this
参数的类型为(new () => T) | typeof Singleton
联合类型。new () => T
表示某个构造函数类型,由于TestClass
继承自Singleton
,同时getInstance
是静态方法,所以这里的new () => T
代表的是TestClass
类。使用了new () => T
,就能告诉编辑器当前使用了哪个类,但是这样一来构造函数使用protected
或者private
修饰时会在编译阶段报错,可能会提示Cannot assign a 'protected' constructor type to a 'public' constructor type.
,意思是无法将“protected”构造函数类型分配给“public”构造函数类型。这时候我们可以指定this
的类型为(new () => T) | typeof Singleton
联合类型。typeof Singleton
表示Singleton
类的类型,因为在类的内部可以实例化受保护的或者私有的构造函数,从而规避掉上述错误。 - 将构造函数用
protected
修饰依然无法避免某些大聪明在子类内部实例化,所以构造函数内部使用了_instantiateByGetInstance
判断是否是通过getInstance
方法进行实例化的。当然这个问题只能在运行期间被发现。
class TestClass extends Singleton {
testFunc() {
new TestClass();
}
}