7

I'm trying to do my implementation of singleton pattern in JS ES6 class. Here is what I wrote so far:

let instance;

export class TestClass{

    constructor(){
        if(new.target){
            throw new Error(`Can't create instance of singleton class with new keyword. Use getInstance() static method instead`);
        }
    }
    testMethod(){
        console.log('test');
    }
    static getInstance(){
        if(!instance) {
            instance = TestClass.constructor();
        }

        return instance;
    }
}

However, when I call static method TestClass.getInstance(), I'm not getting instance of class object, I'm getting

ƒ anonymous() {

}

function, without access to testMethod. I can't find error in my code - help will be greatly appreciated.

Furman
  • 2,017
  • 3
  • 25
  • 43
  • `instance = new TestClass();` – Keith Apr 15 '18 at 20:32
  • A singleton in javascript is called object. – Jonas Wilms Apr 15 '18 at 20:33
  • I don't want to use new keyword, that's why I throw error in constructor if it was called with usage of "new". @JonasW. Yes I know, simplest singleton is simple JS object {}. I want to use ES6 classes. – Furman Apr 15 '18 at 20:34
  • Just don't export the class. Only export the `getInstance()` function that does what you want. Or, perhaps even better, just create and export the singleton, that's all. One call still call all the methods on the singleton even though you didn't export the class itself. – jfriend00 Apr 15 '18 at 20:39
  • `I don't want to use new keyword` , then just export an object and not the class. `export new TestClass()` , also if your doing this for lazy construction, then export a wrapper function to export the object. – Keith Apr 15 '18 at 20:53

3 Answers3

7

TestClass is the constructor function. TestClass.constructor is the builtin Function, which when called constructs a new empty function (what you are logging).

The TestClass constructor can also be accessed as TestClass.prototype.constructor, that's what you probably meant:

static getInstance(){
    if (!instance) {
        instance = TestClass.prototype.constructor();
    }
    return instance;
}

This will of course throw an exception that you cannot call class constructors without new.

You also should simplify to new TestClass. Or even better, in case you want to support subclassing, new this - notice that this in static method refers to the class (constructor) itself.

I'm trying to do my implementation of singleton pattern in JS ES6 class

Please don't. Singletons are bad practice. If your class doesn't have any state, and there's just one instance anyway, don't use a class. Just write

export function testMethod() {
    console.log('test');
}
// Yes, that's the whole file!

If you insist on lazily constructing the module, I would recommend

let instance;
/*default*/ export function getInstance() {
    return instance || (instance = { // use a simple object literal
        testMethod(){
            console.log('test');
        }
    });
}

That said, if you insist on making a "private" constructor I would pass a token:

const internal = Symbol("creation token for TestClass");
export class TestClass {
    constructor(token) {
        if(token !== internal) {
            throw new Error("Please use the TestClass.getInstance() static method instead");
        }
    }
    …
    static getInstance(){
        return new TestClass(internal); // make sure not to call `this`, otherwise subclassing could leak the token
    }
}

But you should never really need that.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • So it's not possible to create ES6 class object without using "new" keyword? – Furman Apr 15 '18 at 20:38
  • That's a very nice trick with this Symbol token - thank you for detailed answer. – Furman Apr 15 '18 at 20:46
  • Why even export the class at all? Why not just export a factory function that returns the singleton. Then, the constructor can just be a normal constructor that is only used within the module to create the original singleton. The public calls the factory function which always returns the singleton. – jfriend00 Apr 15 '18 at 20:50
  • Yes I know there are many different ways to achieve similiar effect. ES6 singleton implementation was done for educational purposes only. – Furman Apr 15 '18 at 20:53
  • @jfriend00 normal constructor doesn't work, as otherwise anyone could do `new (getInstance().constructor)`. But yeah, whether `getInstance` is exported or the whole class doesn't make much of a difference (same for other static methods). – Bergi Apr 15 '18 at 20:59
  • Lots of ways to avoid that if that's a concern. My point is that what the OP is trying to do and what you're showing is just way more complicated than needed to merely export a singleton. There's really just no reason to export the class at all. – jfriend00 Apr 15 '18 at 21:03
  • @jfriend00 Yes, while my personal recommendation is to always avoid singleton *instances*, I updated the answer to with a simple way to instantiate a singleton object. – Bergi Apr 15 '18 at 21:04
  • OK. It's a little hard in your answer to tell what you think is the simple and recommended option. – jfriend00 Apr 15 '18 at 21:07
1

You haven't created an instance of the TestClass, you've just assigned your instance variable as the constructor function of the TestClass.

I typically would create the singleton like this if I need to:

class TestClass {
  constructor() {

  }

  testMethod() {

  }
}

const instance = new TestClass();

export default instance;
Dan D
  • 2,493
  • 15
  • 23
1

The problem is that ES6 Class constructors cannot be invoked without 'new' - your new.target test is superfluous. If you want to keep your Class syntax, you could do something like the following to ensure that only your module has the ability to create the class:

let instance;
let creating = false;
class TestClass{
  constructor(key) {
    if(!creating) {
      throw new Error(`Can't create instance of singleton class with new keyword. Use getInstance() static method instead`);
    }
  }
  testMethod() {
    console.log('test');
  }
  static getInstance() {
    if(!instance) {
      creating = true;
      instance = new TestClass();
      creating = false;
    }
    return instance;
  }
}

const theInst = TestClass.getInstance();
theInst.testMethod();
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320