ChatGPT解决这个技术问题 Extra ChatGPT

如何在 TypeScript 中定义单例

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


A
Alex

从 TS 2.0 开始,我们可以定义 visibility modifiers on constructors,所以现在我们可以在 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 的约束消失并且构造函数不会被隐藏。


构造函数可以是私有的吗?
@Expertwannabe 现在可在 TS 2.0 中使用:github.com/Microsoft/TypeScript/wiki/…
这是我的首选答案!谢谢你。
@KimchiMan 如果该项目曾经在非打字稿环境中使用,例如导入到 JS 项目中,则该类将无法防止进一步实例化。它仅适用于纯 TS 环境,不适用于 JS 库开发
@lony 只需使 Instance 成为一个普通的静态方法(删除'get'),它从您将所需参数传递给构造函数的位置获取所需的参数,该构造函数也接受所需的参数。
M
Muhammad Salman Siddiqi

TypeScript 中的单例类通常是一种反模式。您可以简单地使用 namespaces

无用的单例模式

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

现在很好,为什么单例被认为是反模式?考虑这种方法codebelt.com/typescript/typescript-singleton-pattern
我想知道为什么 TypeScript 中的单例也被认为是一种反模式。而且,如果它没有任何构造函数参数,为什么不使用 export default new Singleton()
命名空间解决方案看起来更像是一个静态类,而不是单例
它的行为相同。在C# 中,您不能将静态类当作一个值来传递(即,就好像它是一个单例类的实例),这限制了它的用处。在 TypeScript 中,您可以像实例一样传递命名空间。这就是为什么你不需要单例类。
将命名空间用作单例的一个限制是它不能(据我所知)实现接口。你同意这个@ryan
c
codeBelt

我发现的最好方法是:

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/


为什么不将构造函数设为私有?
我认为这篇文章早于在 TS 中拥有私有构造函数的能力。 github.com/Microsoft/TypeScript/issues/2341
我喜欢这个答案。私有构造器在开发过程中非常有用,但是如果将转译的 TS 模块导入到 JS 环境中,构造器仍然可以访问。使用这种方法,它几乎可以防止滥用......除非 SingletonClass['_instance'] 设置为 null/undefined
链接已损坏。我认为这是实际链接:codebelt.github.io/blog/typescript/typescript-singleton-pattern
最好将“new SingletonClass()”放在方法“getInstance”中,否则构造函数将在导入此类的位置执行,我们不希望在某些情况下发生这种情况。
m
maja

以下方法创建了一个可以像传统类一样使用的 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();


这是它应该实施的方式。如果有 1 件事我不同意四人帮的观点——而且可能只有 1——那就是单例模式。也许,C/++ 阻碍了人们以这种方式设计它。但是如果你问我,客户端代码不应该知道或关心它是否是单例。客户端仍应实现 new Class(...) 语法。
不同意 Cody,新实例必须是新实例,否则开发人员会做出错误的假设。
我不完全同意上述说法(胡安)。 getInstance() 这里有一些副作用(它创建一个新对象),而它的名字暗示它根本不应该(getXXX)。我相信这甚至是最糟糕的。
R
Romain Bruckert

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

// shout.ts
class ShoutSingleton {
  helloWorld() { return 'hi'; }
}

export let Shout = new ShoutSingleton();

用法

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

我收到以下错误消息:导出的变量“Shout”已经或正在使用私有名称“ShoutSingleton”。
您还必须导出类“ShoutSingleton”,错误就会消失。
没错,我也很惊讶。为什么还要费心上课呢?单身人士应该隐藏他们的内部运作。为什么不只导出函数 helloWorld?
有关详细信息,请参阅此 github 问题:github.com/Microsoft/TypeScript/issues/6307
猜猜没有什么能阻止用户创建一个新的 Shout
F
Flavien Volken

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

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

Test example:

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

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


当我做 new MySingleton() 时会发生什么,比如 5 次?您的代码是否保留单个实例?
你永远不应该使用“new”:正如 Alex 所写,构造函数应该是“private”,防止执行“new MySingleton()”。正确的用法是使用 MySingleton.getInstance() 获取实例。 AKAIK 没有构造函数(就像在我的例子中一样)=一个公共的空构造函数
“你永远不应该使用“新”——这正是我的观点:“。但是您的实施如何阻止我这样做?我看不到您的班级中有私有构造函数的任何地方?
@HlawulekaMAS 我没有……因此我编辑了答案,请注意在 TS 2.0 之前无法使用私有构造函数(即在我先写答案的时候)
“即在我先写答案的时候” - 有道理。凉爽的。
b
bingles

您可以为此使用类表达式(我相信从 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();

s
sanye

我想也许使用泛型是击球手

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();

对于 Typescript 3.0 及更高版本,您可以使用 type 'unknown' 作为 static Instance 方法的返回类型和 _instance 的类型,然后像 MapManager.Instance(MapManager) as MapManager 一样使用。这可以消除 eslint 的类型不匹配错误。
D
Daniel de Andrade Varela

我的解决方案:

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());
    }
}

Test in TS Playground


在构造函数中,您可以 return Modal._instance 代替异常。这样,如果您 new 该类,您将获得现有对象,而不是新对象。
k
kenny

您还可以使用函数 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;

肯尼,关于 freeze() 的好点,但有两个注意事项:(1) 在你 freeze(singleton) 之后,你仍然可以修改 singleton.data .. 你不能添加其他属性(比如 data2),但重点是 freeze( ) 不是深度冻结 :) 和 (2) 你的 Singleton 类允许创建多个实例(例如 obj1 = new Singleton(); obj2 = new Singleton();),所以你的 Singleton 不是 Singleton :)
如果您在其他文件中导入单例类,您将始终获得相同的实例,并且“数据”中的数据将在所有其他导入之间保持一致。这对我来说是一个单身人士。确保导出的 Singleton 实例只创建一次的冻结。
肯尼,(1)如果你在其他文件中导入你的类,你将不会得到实例。通过导入,您只需将类定义引入范围,以便您可以创建新实例。然后,您可以在一个文件或多个文件中创建给定类的 >1 个实例,这违背了单例想法的全部目的。 (2) 来自文档: Object.freeze() 方法冻结一个对象。无法再更改冻结的对象;冻结对象可防止向其添加新属性。 (引用结束)这意味着 freeze() 不会阻止您创建多个对象。
是的,但在这种情况下它会,因为导出的成员已经是一个实例。实例保留数据。如果您也将导出放在该类上,那么您是对的,您可以创建多个实例。
@kenny 如果您知道要导出一个实例,为什么还要在构造函数中使用 if (!this.instance) ?如果您在导出之前创建了多个实例,这只是额外的预防措施吗?
E
Eagerestwolf

我发现了一个 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 也会认为它没有被定义。这可以通过禁用严格模式来解决,但如果可能的话,我不希望这样做。我应该指出这个方法还有另一个缺点,因为构造函数实际上是被调用的,每次它执行时都会在技术上创建另一个实例,但无法访问。从理论上讲,这可能会导致内存泄漏。


M
Milkncookiez
/**
 * 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();

这个答案与所有其他为延迟初始化提供 getInstance() 方法的答案有何不同?
它对代码的每个步骤都有更好的解释。如果我添加了另一个与解决方案重叠的答案,为什么这很重要?
我同意你的格式和评论很好。但这很重要,因为这个问题已经有 23 个答案,而且每个月都会有新的答案。最好不要重复现有的答案,而是对已经存在的答案进行评论以澄清它们或提出问题。或者编辑现有的答案,如果它们可以改进的话。
A
Adrian Guerrero

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

首先你需要一个 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();
}

很好!,现在在任何你需要的地方初始化或导入:

/// <reference path="./navigation/Router.ts" />

import router = navigation.Router;

router.init();
router.init(); // Throws error!.

以这种方式执行单例的好处是您仍然可以使用 typescript 类的所有优点,它为您提供了很好的智能感知,单例逻辑以某种方式保持分离,并且在需要时很容易删除。


D
Dominic Lee

在 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


T
TheGeekZn

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

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()


为什么有人会在没有实际评论改进的情况下对答案投反对票?我们是一个社区
M
Maksim Nesterenko

在实现了一个经典模式之后

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 包中的 this 基于代理的实现足够简洁。


J
JesperA

这是另一种使用 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();

查看demo


添加 Instance 变量的原因是什么?您可以简单地将变量和函数直接放在 App.Counter 下。
@fyaa 是的,您可以,但变量和函数直接位于 App.Counter 下,但我认为这种方法更符合单例模式 en.wikipedia.org/wiki/Singleton_pattern
C
Ciberman

另一种选择是在模块中使用符号。这样你就可以保护你的类,如果你的 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

s
sergzach

不是纯单例(初始化可能不是惰性的),而是借助 namespace 的类似模式。

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

访问单例对象:

MyClass.instance

S
Sorin Veștemean

这是最简单的方法

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;
  }
}

R
Rafiq

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

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)
}

M
Margesh Patel

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

我认为下面是更实际的解决方案。

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);

u
user487779
namespace MySingleton {
  interface IMySingleton {
      doSomething(): void;
  }
  class MySingleton implements IMySingleton {
      private usePrivate() { }
      doSomething() {
          this.usePrivate();
      }
  }
  export var Instance: IMySingleton = new MySingleton();
}

这样我们就可以应用一个界面,这与 Ryan Cavanaugh 接受的答案不同。