如何在TypeScript中定义Singleton


128

在TypeScript中为类实现Singleton模式的最佳和最方便的方法是什么?(有和没有惰性初始化)。

Answers:


87

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

55
到现在为止很好,为什么将单例视为反模式?考虑这种方法codebelt.com/typescript/typescript-singleton-pattern
Victor

21
我想知道为什么TypeScript中的Singletons也被视为反模式。而且,如果它没有任何构造函数参数,为什么不export default new Singleton()呢?
emzero

23
命名空间解决方案看起来更像是一个静态类,而不是一个单例
MihaiRăducanu16年

6
它的行为相同。在C#中,您不能像传递静态值一样传递静态类(即好像它是单例类的实例),这限制了它的用途。在TypeScript中,您可以像实例一样传递名称空间。这就是为什么您不需要单例类的原因。
Ryan Cavanaugh

13
使用名称空间作为单例的限制是(据我所知)它不能实现接口。您是否同意这个@ryan
Gabe O'Leary

182

从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的约束消失了,构造函数也不会被隐藏。


2
构造函数可以是私有的吗?
专家希望成为

2
@Expertwannabe现在在TS 2.0中可用:github.com/Microsoft/TypeScript/wiki/…–
Alex

3
这是我的首选答案!谢谢。
Martin Majewski

1
事实是,多个实例的原因是节点模块分辨率受到阻碍。因此,如果要在节点中创建单例,请确保已将其考虑在内。我最终在src目录下创建了一个node_modules文件夹,并将单例放在其中。
webteckie

3
@KimchiMan如果项目曾经在非打字稿环境中使用过,例如导入到JS项目中,则该类将无法防止进一步的实例化。它仅在纯TS环境中工作,但不适用于JS库开发
Drenai

39

我发现的最好方法是:

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/


3
为什么不将构造函数设为私有?
菲尔·曼德

4
我认为该职位早于在TS中拥有私有构造函数的能力。github.com/Microsoft/TypeScript/issues/2341
Trevor

我喜欢这个答案。私有构造函数在开发过程中很棒,但是如果将转译的TS模块导入到JS环境中,则仍然可以访问该构造函数。使用这种方法,几乎​​可以防止滥用。...除非SingletonClass ['_ instance']设置为null / undefined
Drenai

链接断开。我认为这是实际的链接:codebelt.github.io/blog/typescript/typescript-singleton-pattern
El Asiduo

24

下面的方法创建一个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件事情,我不同意“四人帮”的论点-可能只有 1件-其“单例模式”。也许,C / ++阻碍了这种设计。但是,如果您问我,客户端代码不应该知道或关心它是否为Singleton。客户端仍应实现new Class(...)语法。
科迪

16

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

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

export let Shout = new ShoutSingleton();

用法

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

我收到以下错误消息:导出变量'Shout'具有或正在使用私有名称'ShoutSingleton'。
Twois

3
您也必须导出类'ShoutSingleton',错误消失。
Twois

是的,我也很惊讶。为什么还要上课呢?单身人士应该隐藏自己的内部运作方式。为什么不只导出函数helloWorld?
奥列格·杜林

有关更多信息,请参见此github问题:github.com/Microsoft/TypeScript/issues/6307
Ore4444 '18

5
猜猜没有什么阻止用户创建新Shout
dalore

7

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

7

将以下6行添加到任何类中,使其成为“ Singleton”。

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答案。


new MySingleton()说5次后会怎样?您的代码是否保留一个实例?
Hlawuleka MAS

您永远不要使用“ new”:正如Alex所写的那样,构造函数应该是“ private”,从而避免执行“ new MySingleton()”。正确的用法是使用MySingleton.getInstance()获取实例。AKAIK没有构造函数(例如在我的示例中)=一个公共的空构造函数
Flavien Volken

“您永远不应该使用“新”-正是我的观点:”。但是您的实施方式如何阻止我这样做?我看不到您的班级中有私有构造函数的地方吗?
Hlawuleka MAS

@HlawulekaMAS我没有…因此,我编辑了答案,请注意,在TS 2.0之前(即在我首先写下答案时)不可能使用私有构造函数
Flavien Volken

“即是我第一次写答案的时候”-很有道理。凉。
Hlawuleka MAS

3

我认为也许使用仿制药会更糟

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

3

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

肯尼(Kenny),关于frozen()的要点,但有两个注意事项:(1)在冻结(singleton)之后,您仍然可以修改singleton.data ..您不能添加其他属性(如data2),但要点是冻结( )不是深度冻结:)和(2)您的类Singleton允许创建多个实例(示例obj1 = new Singleton(); obj2 = new Singleton();),因此您的Singleton不是Singleton:)
Dmitry Shevkoplyas

如果将Singleton类导入其他文件,则将始终获得相同的实例,并且“ data”中的数据在所有其他导入之间将保持一致。对我来说是单身。确保导出的Singleton实例仅创建一次。
肯尼

肯尼(1),如果将类导入其他文件,则不会获得实例。通过导入,您只需将类定义引入作用域,以便可以创建新实例。然后,您可以在一个文件或多个文件中创建给定类的> 1个实例,这违背了单例想法的全部目的。(2)从docs:Object.freeze()方法冻结对象。冻结的对象无法再更改;冻结对象可防止向其添加新属性。(引号的结尾)这意味着Frozen()不会阻止您创建多个对象。
德米特里Shevkoplyas

是的,但在这种情况下会是这样,因为导出的成员已经是一个实例。实例保留数据。如果也对类进行导出,那是对的,您可以创建多个实例。
肯尼

@kenny如果您知道要导出实例,为什么还要if (!this.instance)在构造函数中麻烦呢?如果您在导出之前创建了多个实例,这是否只是一种额外的预防措施?
亚历克斯

2

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


1

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

首先,您需要在“ ./utils/Singleton.ts”中添加一个Singleton类:

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!.

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


1

我的解决方案:

export default class Modal {
    private static _instance : Modal = new Modal();

    constructor () {
        if (Modal._instance) 
            throw new Error("Use Modal.instance");
        Modal._instance = this;
    }

    static get instance () {
        return Modal._instance;
    }
}

1
在构造函数中,可以代替异常return Modal._instance。这样,如果您new使用该类,则将获得现有对象,而不是新对象。
MihaiRăducanu16年

1

在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()在其他任何类中进行引用。但是请记住,因为这是一个静态类,所以它没有构造函数,所以我通常使用intialise()从导入Singleton的类调用的方法:

import {YourSingleton} from 'singleton.ts';

YourSingleton.initialise(params);
let _result:bar = YourSingleton.doThing();

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


0

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

观看演示


添加Instance变量的原因是什么?您只需将变量和函数直接放在App.Counter
fyaa'2

@fyaa是的,您可以但是直接在App.Counter下的变量和函数,但是我认为这种方法更适合于单例模式en.wikipedia.org/wiki/Singleton_pattern
JesperA

0

另一种选择是在模块中使用符号。这样,即使您的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

0

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

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

访问Singleton对象:

MyClass.instance

0

这是最简单的方法

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

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

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


-1

在检查完该线程并尝试了上述所有选项之后,我选择了可​​以通过适当的构造函数创建的Singleton:

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


为什么有人在不对改进发表评论的情况下对答案投下反对票?我们是一个社区
TheGeekZn
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.