如何在 TypeScript 中定义 Singleton



在TypeScript中为类实现单例模式的最佳和最方便的方法是什么?(带和不带延迟初始化)。

从 TS 2.0 开始,我们就能够在构造函数上定义可见性修饰符,所以现在我们可以像习惯于其他语言一样在 TypeScript 中执行单例操作。

给出的例子:

class MyClass
{
private static _instance: MyClass;
private constructor()
{
//...
}
public static get Instance()
{
// Do you need arguments? Make it a regular static method instead.
return this._instance || (this._instance = new this());
}
}
const myClassInstance = MyClass.Instance;

感谢您@Drenai指出,如果您使用原始编译的 javascript 编写代码,您将无法防止多次实例化,因为 TS 的约束消失并且构造函数不会被隐藏。

TypeScript 中的 Singleton 类通常是一种反模式。您可以改用命名空间。

无用的单例模式

class Singleton {
/* ... lots of singleton logic ... */
public someMethod() { ... }
}
// Using
var x = Singleton.getInstance();
x.someMethod();

命名空间等效项

export namespace Singleton {
export function someMethod() { ... }
}
// Usage
import { SingletonInstance } from "path/to/Singleton";
SingletonInstance.someMethod();
var x = SingletonInstance; // If you need to alias it for some reason

我发现的最好的方法是:

class SingletonClass {
private static _instance:SingletonClass = new SingletonClass();
private _score:number = 0;
constructor() {
if(SingletonClass._instance){
throw new Error("Error: Instantiation failed: Use SingletonClass.getInstance() instead of new.");
}
SingletonClass._instance = this;
}
public static getInstance():SingletonClass
{
return SingletonClass._instance;
}
public setScore(value:number):void
{
this._score = value;
}
public getScore():number
{
return this._score;
}
public addPoints(value:number):void
{
this._score += value;
}
public removePoints(value:number):void
{
this._score -= value;
}
}

以下是您的使用方式:

var scoreManager = SingletonClass.getInstance();
scoreManager.setScore(10);
scoreManager.addPoints(1);
scoreManager.removePoints(2);
console.log( scoreManager.getScore() );

https://codebelt.github.io/blog/typescript/typescript-singleton-pattern/

以下方法创建一个可以像传统类一样使用的 Singleton 类:

class Singleton {
private static instance: Singleton;
//Assign "new Singleton()" here to avoid lazy initialisation
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this. member = 0;
Singleton.instance = this;
}
member: number;
}

每个new Singleton()操作将返回相同的实例。但是,这可能是用户意外的。

以下示例对用户更透明,但需要不同的用法:

class Singleton {
private static instance: Singleton;
//Assign "new Singleton()" here to avoid lazy initialisation
constructor() {
if (Singleton.instance) {
throw new Error("Error - use Singleton.getInstance()");
}
this.member = 0;
}
static getInstance(): Singleton {
Singleton.instance = Singleton.instance || new Singleton();
return Singleton.instance;
}
member: number;
}

用法:var obj = Singleton.getInstance();

我很惊讶没有在这里看到以下模式,它实际上看起来非常简单。

// shout.ts
class ShoutSingleton {
helloWorld() { return 'hi'; }
}
export let Shout = new ShoutSingleton();

用法

import { Shout } from './shout';
Shout.helloWorld();

将以下 6 行添加到任何类以使其成为"单例"。

class MySingleton
{
private constructor(){ /* ... */}
private static _instance: MySingleton;
public static getInstance(): MySingleton
{
return this._instance || (this._instance = new this());
};
}

测试示例:

var test = MySingleton.getInstance(); // will create the first instance
var test2 = MySingleton.getInstance(); // will return the first instance
alert(test === test2); // true

[编辑]:如果您希望通过属性而不是方法获取实例,请使用 Alex 答案。

[编辑 2]: 奖励:如果您想在一行中从现有类生成单例,您可以使用此类构建器:

function AsSingleton<T extends new (...args: any[]) => any>(Aclass: T, ...args: ConstructorParameters<T>) {
return class extends Aclass {
static #instance: InstanceType<T>;
public static getInstance(): InstanceType<T> {
if (!this.#instance) {
this.#instance = new Aclass(...args);
}
return this.#instance;
}
} as Omit<T,"new"> & {getInstance:()=>InstanceType<T>};
}

查看此代码段以获取示例(甚至更高级)

用法:const AClassSingleton = AsSingleton(AClass);

我的解决方案:

export default class Singleton {
private static _instance: Singleton = new Singleton();
constructor() {
if (Singleton._instance)
throw new Error("Use Singleton.instance");
Singleton._instance = this;
}
static get instance() {
return Singleton._instance;
}
}

2021 更新

现在构造函数可以是私有的

export default class Singleton {
private static _instance?: Singleton;
private constructor() {
if (Singleton._instance)
throw new Error("Use Singleton.instance instead of new.");
Singleton._instance = this;
}
static get instance() {
return Singleton._instance ?? (Singleton._instance = new Singleton());
}
}

在 TS 操场上测试

你可以为此使用类表达式(我相信从 1.6 开始)。

var x = new (class {
/* ... lots of singleton logic ... */
public someMethod() { ... }
})();

或者使用名称(如果您的类需要在内部访问其类型)

var x = new (class Singleton {
/* ... lots of singleton logic ... */
public someMethod(): Singleton { ... }
})();

另一种选择是使用一些静态成员在单例中使用本地类

class Singleton {
private static _instance;
public static get instance() {
class InternalSingleton {
someMethod() { }
//more singleton logic
}
if(!Singleton._instance) {
Singleton._instance = new InternalSingleton();
}
return <InternalSingleton>Singleton._instance;
}
}
var x = Singleton.instance;
x.someMethod();

我认为也许使用泛型是打击者

class Singleton<T>{
public static Instance<T>(c: {new(): T; }) : T{
if (this._instance == null){
this._instance = new c();
}
return this._instance;
}
private static _instance = null;
}

如何使用

步骤1

class MapManager extends Singleton<MapManager>{
//do something
public init():void{ //do }
}

步骤2

MapManager.Instance(MapManager).init();

你也可以使用函数Object.Freeze()。它简单易行:

class Singleton {
instance: any = null;
data: any = {} // store data in here
constructor() {
if (!this.instance) {
this.instance = this;
}
return this.instance
}
}
const singleton: Singleton = new Singleton();
Object.freeze(singleton);
export default singleton;

我找到了Typescript编译器完全可以接受的新版本,我认为更好,因为它不需要不断调用getInstance()方法。

import express, { Application } from 'express';
export class Singleton {
// Define your props here
private _express: Application = express();
private static _instance: Singleton;
constructor() {
if (Singleton._instance) {
return Singleton._instance;
}
// You don't have an instance, so continue
// Remember, to set the _instance property
Singleton._instance = this;
}
}

这确实带来了不同的缺点。如果您的Singleton确实具有任何属性,则 Typescript 编译器将抛出适合,除非您使用值初始化它们。这就是为什么我在示例类中包含_express属性的原因,因为除非您使用值初始化它,即使您稍后在构造函数中分配它,Typescript 也会认为它尚未定义。这可以通过禁用严格模式来解决,但如果可能的话,我宁愿不这样做。我应该指出这种方法还有另一个缺点,因为构造函数实际上是被调用的,每次调用时,都会在技术上创建另一个实例,但无法访问。从理论上讲,这可能会导致内存泄漏。

在实现经典模式后,例如

class Singleton {
private instance: Singleton;

private constructor() {}
public getInstance() {
if (!this.instance) { 
this.instance = new Singleton();
}
return this.instance;
}
}

我意识到,如果你也想让其他类成为单例,这是毫无用处的。它不可扩展。你必须为你想成为单例的每个类编写单例内容。

用于救援的装饰器。

@singleton
class MyClassThatIsSingletonToo {}

你可以自己编写装饰器,也可以从 npm 中获取一些。我发现这个来自 @keenondrums/singleton 包的基于代理的实现足够整洁。

/**
* The Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*/
class Singleton {
private static instance: Singleton;
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
private constructor() { }
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
/**
* Finally, any singleton should define some business logic, which can be
* executed on its instance.
*/
public someBusinessLogic() {
// ...
}
}
/**
* The client code.
*/
function clientCode() {
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
if (s1 === s2) {
console.log('Singleton works, both variables contain the same instance.');
} else {
console.log('Singleton failed, variables contain different instances.');
}
}
clientCode();

这可能是在打字稿中制作单例的最长过程,但在较大的应用程序中对我来说效果更好。

首先,你需要一个 Singleton 类,比如说 "./utils/Singleton.ts">

module utils {
export class Singleton {
private _initialized: boolean;
private _setSingleton(): void {
if (this._initialized) throw Error('Singleton is already initialized.');
this._initialized = true;
}
get setSingleton() { return this._setSingleton; }
}
}

现在假设你需要一个路由器单例"./navigation/Router.ts">

/// <reference path="../utils/Singleton.ts" />
module navigation {
class RouterClass extends utils.Singleton {
// NOTICE RouterClass extends from utils.Singleton
// and that it isn't exportable.
private _init(): void {
// This method will be your "construtor" now,
// to avoid double initialization, don't forget
// the parent class setSingleton method!.
this.setSingleton();
// Initialization stuff.
}
// Expose _init method.
get init { return this.init; }
}
// THIS IS IT!! Export a new RouterClass, that no
// one can instantiate ever again!.
export var Router: RouterClass = new RouterClass();
}

Nice!,现在在您需要的任何位置初始化或导入:

/// <reference path="./navigation/Router.ts" />
import router = navigation.Router;
router.init();
router.init(); // Throws error!.

以这种方式做单例的好处是,你仍然使用打字稿类的所有美感,它给你很好的智能感,单例逻辑保持某种方式分离,如果需要,很容易删除。

在 Typescript 中,不一定必须遵循new instance()Singleton 方法。导入的无构造函数静态类也可以同样工作。

考虑:

export class YourSingleton {
public static foo:bar;
public static initialise(_initVars:any):void {
YourSingleton.foo = _initvars.foo;
}
public static doThing():bar {
return YourSingleton.foo
}
}

您可以导入类并在任何其他类中引用YourSingleton.doThing()。但请记住,因为这是一个静态类,所以它没有构造函数,所以我通常使用从导入 Singleton 的类调用的intialise()方法:

import {YourSingleton} from 'singleton.ts';
YourSingleton.initialise(params);
let _result:bar = YourSingleton.doThing();

不要忘记,在静态类中,每个方法和变量也需要是静态的,因此您可以使用完整的类名YourSingleton代替this

在搜索此线程并使用上述所有选项后 - 我使用了一个可以使用适当构造函数创建的单例:

export default class Singleton {
private static _instance: Singleton
public static get instance(): Singleton {
return Singleton._instance
}
constructor(...args: string[]) {
// Initial setup
Singleton._instance = this
}
work() { /* example */ }
}

它需要一个初始设置(main.tsindex.ts),这可以通过new Singleton(/* PARAMS */)轻松实现

然后,在你的代码中的任何位置,只需调用Singleton.instnace;在这种情况下,为了完成work,我会调用Singleton.instance.work()

这是使用IFFE的更传统的JavaScript方法的另一种方法:

module App.Counter {
export var Instance = (() => {
var i = 0;
return {
increment: (): void => {
i++;
},
getCount: (): number => {
return i;
}
}
})();
}
module App {
export function countStuff() {
App.Counter.Instance.increment();
App.Counter.Instance.increment();
alert(App.Counter.Instance.getCount());
}
}
App.countStuff();

观看演示

另一种选择是在模块中使用符号。通过这种方式,您可以保护您的类,即使 API 的最终用户使用的是普通的 Javascript:

let _instance = Symbol();
export default class Singleton {
constructor(singletonToken) {
if (singletonToken !== _instance) {
throw new Error("Cannot instantiate directly.");
}
//Init your class
}
static get instance() {
return this[_instance] || (this[_instance] = new Singleton(_singleton))
}
public myMethod():string {
return "foo";
}
}

用法:

var str:string = Singleton.instance.myFoo();

如果用户正在使用您编译的 API js 文件,如果他尝试手动实例化您的类,也会收到错误:

// PLAIN JAVASCRIPT: 
var instance = new Singleton(); //Error the argument singletonToken !== _instance symbol

不是纯粹的单例(初始化可能不是懒惰的),而是在namespaces 的帮助下类似的模式。

namespace MyClass
{
class _MyClass
{
...
}
export const instance: _MyClass = new _MyClass();
}

访问单例的对象:

MyClass.instance

这是最简单的方法

class YourSingletoneClass {
private static instance: YourSingletoneClass;
private constructor(public ifYouHaveAnyParams: string) {
}
static getInstance() {
if(!YourSingletoneClass.instance) {
YourSingletoneClass.instance = new YourSingletoneClass('If you have any params');
}
return YourSingletoneClass.instance;
}
}

让我们举个例子,我想创建单例类,通过它我可以创建客户端的连接,然后我想在任何地方使用相同的连接客户端。

import nats, { Stan } from 'node-nats-streaming';
class NatsWrapper {
private _client?: Stan;
get client() {
if (!this._client) {
throw new Error('Cannot access NATS client before connecting');
}
return this._client;
}
connect(clusterId: string, clientId: string, url: string) {
this._client = nats.connect(clusterId, clientId, { url });
return new Promise((resolve, reject) => {
this.client.on('connect', (result) => {
console.log('Connected to NATS');
resolve(result);
});
this.client.on('error', (err) => {
reject(err);
});
});
}
}
// since we create and export the instace, it will act like a singleton
export const natsWrapper = new NatsWrapper();

现在,首先在您的 index.ts/app.ts 文件中创建连接,然后您只需在任何地方导入即可访问同一客户端

索引.ts

await natsWrapper.connect(
'ticketing',
'client_id_random_str',
'http://nats-srv:4222'
);

一些文件.ts

import { natsWrapper } from '../nats-wrapper';
const abc = () =>{
console.log(natsWrapper.client)
}

我一直在努力寻找在打字稿中声明单例模式类的合适解决方案。

我认为以下是更实用的解决方案。

class MySingletonClass {
public now:Date = new Date();
public arg:string;
constructor(arg:string) {
this.arg = arg;
// Make singleton
if ('instance' in MySingletonClass) return Object.getOwnPropertyDescriptor(MySingletonClass, 'instance')?.value;
Object.assign(MySingletonClass, { instance: this });
}
}
const a = new MySingletonClass('a');
console.log(a);
const b = new MySingletonClass('b');
console.log(b);
console.log('a === b', a === b);
console.log('a.now === b.now', a.now === b.now);
class Singleton<CREATOR extends () => any>
{
private m_fnGetInstance: () => ReturnType<CREATOR>;
public get Instance() { return this.m_fnGetInstance(); }
public constructor(creator: CREATOR) {
this.m_fnGetInstance = () => {
const instance = creator();
this.m_fnGetInstance = () => instance;
return instance;
};
}
}
const Foo = new Singleton(() => new class {
public bar() {}
});
Foo.Instance.bar();

它很快,因为没有条件分支。

操场

GitHub

您也可以尝试以下代码:

import { Injectable } from '@nestjs/common';
@Injectable()
export class SingletonDemo {
private static _instance: SingletonDemo;
public static getInstance() {
return this._instance || (this._instance = new this());
}
public static resetInstance() {
this._instance = null;
}
}
namespace MySingleton {
interface IMySingleton {
doSomething(): void;
}
class MySingleton implements IMySingleton {
private usePrivate() { }
doSomething() {
this.usePrivate();
}
}
export var Instance: IMySingleton = new MySingleton();
}

通过这种方式,我们可以应用一个接口,这与 Ryan Cavanaugh 接受的答案不同。

最新更新