34

I have defined pure objects in JS which expose certain static methods which should be used to construct them instead of the constructor. How can I make a constructor for my class private in Javascript?

var Score = (function () {

  // The private constructor
  var Score = function (score, hasPassed) {
      this.score = score;
      this.hasPassed = hasPassed;
  };

  // The preferred smart constructor
  Score.mkNewScore = function (score) {
      return new Score(score, score >= 33);
  };

  return Score;
})();

Update: The solution should still allow me to test for x instanceof Score. Otherwise, the solution by @user2864740 of exposing only the static constructor works.

2
  • i suggest you read addyosmani.com/resources/essentialjsdesignpatterns/book/… Commented Feb 12, 2014 at 15:13
  • 2
    If you use class and #constructor you will get SyntaxError: Class constructor may not be a private method (v8) and SyntaxError: bad method definition (SpiderMonkey). Because of this, I assume private constructors are not meant to be part of JS/ES. If you wanted to create a singleton, you could create a private field static #instance = null, in constructor if (YourClass.#instance instanceof YourClass) return YourClass.#instance // or throw error perhaps Commented Jun 26, 2022 at 14:37

8 Answers 8

21

One can use a variable (initializing) inside a closure which can throw an error if the constructor was called directly instead of via a class method:

var Score = (function () {
  var initializing = false;

  var Score = function (score, hasPassed) {
      if (!initializing) {
         throw new Error('The constructor is private, please use mkNewScore.');
      }

      initializing = false;
      this.score = score;
      this.hasPassed = hasPassed;
  };

  Score.mkNewScore = function (score) {
      intializing = true;
      return new Score(score, score >= 33);
  };

  return Score;
})();
Sign up to request clarification or add additional context in comments.

3 Comments

@Bergi Is there a solution which will allow me to say x instanceof Score otherwise?
@Bergi, Throwing can be avoided by providing a graceful fallback: ` if (!initializing) { console.warn('Private constructor was used (FIX THIS BUG)'); return Score.mkNewScore(score); } `
Graceful fallbacks like this lead to fragmentation in the code. If something should not happen, that is a justified use of throwing an error. Otherwise, you get a codebase littered with fallbacks for things that are not desired.
20

In order to create a private constructor in JS, I like to create a private key that is only accessible in the class (function) file and provide a static factory function as the only allowed way to construct said class:

// in PrivateConstructorClass.js

// Use a Symbol as this will always be unique.
// If you don't have Symbol in your runtime,
// use a random string that nobody can reliably guess, 
// such as the current time plus some other random values.
const PRIVATE_CONSTRUCTOR_KEY = Symbol()

class PrivateConstructorClass {
  constructor(arg1, arg2, argN, constructorKey) {
    if (constructorKey !== PRIVATE_CONSTRUCTOR_KEY) {
      throw new Error('You must use the PrivateConstructorClass.create() to construct an instance.')
    }

    this.arg1 = arg1
    this.arg2 = arg2
    this.argN = argN
  }

  static create(arg1, arg2, argN) {
    return new PrivateConstructorClass(arg1, arg2, argN, PRIVATE_CONSTRUCTOR_KEY)
  }
}

// From Another JS File:

try {
  const myFailedInstanceA = new PrivateConstructorClass('foo', 123, {
    size: 'n'
  })
} catch (err) {
  console.error('Failed:', err.message)
}

const myFactoryInstance = PrivateConstructorClass.create('foo', 123, {
  size: 'n'
})

console.log('Success:', myFactoryInstance)

2 Comments

No need to have a forgeable random string – just use an object.
@dumbass ah just that's definitely more straightforward if you don't have access to Symbol
11

Is there a solution which will allow me to say x instanceof Score?

Yes. Conceptually, @user2864740 is right, but for instanceof to work we need to expose (return) a function instead of a plain object. If that function has the same .prototype as our internal, private constructor, the instanceof operator does what is expected:

var Score  = (function () {

  // the module API
  function PublicScore() {
    throw new Error('The constructor is private, please use Score.makeNewScore.');
  }

  // The private constructor
  var Score = function (score, hasPassed) {
      this.score = score;
      this.hasPassed = hasPassed;
  };

  // Now use either
  Score.prototype = PublicScore.prototype; // to make .constructor == PublicScore,
  PublicScore.prototype = Score.prototype; // to leak the hidden constructor
  PublicScore.prototype = Score.prototype = {…} // to inherit .constructor == Object, or
  PublicScore.prototype = Score.prototype = {constructor:null,…} // for total confusion :-)

  // The preferred smart constructor
  PublicScore.mkNewScore = function (score) {
      return new Score(score, score >= 33);
  };

  return PublicScore;
}());

> Score.mkNewScore(50) instanceof Score
true
> new Score
Error (…)

8 Comments

Clearly a simpler solution than the one provided by @musically_ut. One that does not throw either. Btw, throwing errors is ugly…
Is this still the best way to achieve this in 2020?
@Neutrino It still would work, but you'll probably want to use class syntax. Today, I'd pass a const token = Symbol() as an extra constructor parameter, throw the exception when a check for it fails, and have the token scoped to only those functions that should get access.
I suspected something class syntax based would be more appropriate these days, but I'm still learning Javscript and frankly I'm struggling to follow some of this stuff. Would you mind updating your answer with a quick example please?
@Neutrino If you're still learning JavaScript, I would recommend to simply not try making constructors private.
|
6

Simply don't expose the constructor function. The core issue with the original code is the "static method" is defined as a property of the constructor (which is used as a "class") as opposed a property of the module.

Consider:

return {
    mkNewScore: Score.mkNewScore
    // .. and other static/module functions
};

The constructor can still be accessed via .constructor, but .. meh. At this point, might as well just let a "clever user" have access.

return {
    mkNewScore: function (score) {
        var s = new Score(score, score >= 33);
        /* Shadow [prototype]. Without sealing the object this can
           be trivially thwarted with `del s.constructor` .. meh.
           See Bergi's comment for an alternative. */
        s.constructor = undefined;
        return s;
    }
};

4 Comments

Just put Score.prototype = {} instead of shadowing the inherited constructor property…
This does not allow me to use x instanceof Score. Is there a workaround for that? If there isn't, then may I please mark this question as unanswered? I apologize for the prematurely accepting it. :(
@musically_ut Using instanceof requires access to the constructor. I vary rarely - as in, not within the last year - use instanceof and instead rely on duck-typing in JavaScript. (Feel free to change your answer, it's not the end of SO ;-)
This difference becomes more significant for me since I am using typescript and it lacks Union types. The only way to ensure certain operations are safe is to use instanceof.
0

Just to post a modern version of the solution with Class, static, private.

// define
class Score {
  static #pri = {} // no one can get your #pri from the outside

  // The preferred smart constructor
  static mkNewScore(score) {
    if (score >= 33) {
      return new Score(Score.#pri, score)
    } else {
      throw new Error("The score must be >= 33")
    }
  }

  // The private constructor
  #construct(score) {
    this.score = score
    this.hasPassed = true
  }

  // default parameter to let outside do not know your length of arguments.
  constructor(pri = 0, ...args) {
    if (pri !== Score.#pri)
      throw new TypeError("Failed to construct 'Score': Illegal constructor")
    this.#construct(...args) // hide your constructor function content
  }
}

// testing

try {
  console.log(new Score({}, 50).score)
} catch (e) {
  console.warn(e.message)
}

try {
  console.log(Score.mkNewScore(20).score)
} catch (e) {
  console.warn(e.message)
}

try {
  console.log(Score.mkNewScore(40).score)
} catch (e) {
  console.warn(e.message)
}


However, people can still use console.log(Score) to see your class implementation.

If you want to hide everything, then use the following improved implementation.

// define
const Score = (() => {
  class Seal {
    // The preferred smart constructor
    static mkNewScore(score) {
      if (score >= 33) { 
        return new this(Seal, score)
      } else {
        throw new Error("The score must be >= 33")
      }
    }

    // The private constructor
    #construct(score) {
      this.score = score
      this.hasPassed = true
    }

    // default parameter to let outside do not know your length of arguments.
    constructor(pri = 0, ...args) {
      if (pri !== Seal)
        throw new TypeError("Failed to construct 'Score': Illegal constructor")
      this.#construct(...args) // hide your constructor function content
    }
  }

  class Score extends Seal {}

  return Score
})()

// testing

try {
  console.log(new Score({}, 50).score)
} catch (e) {
  console.warn(e.message)
}

try {
  console.log(Score.mkNewScore(20).score)
} catch (e) {
  console.warn(e.message)
}

try {
  console.log(Score.mkNewScore(40).score)
} catch (e) {
  console.warn(e.message)
}

Comments

-1

Another possible simple approach is to use predicate function instead of instanceof. For typescript it can be a type guard and type synonym instead of a class can be exported:

// class is private
class _Score {
  constructor() {}
}

export type Score = _Score

export function isScore(s): s is Score {
  return s instanceof _Score
}

Comments

-2

So to be fair the simplest answer is usually the best. An object literal is always a single instance. Not much reason for anything more complex other than, perhaps allocation of memory on demand.

That being said, here is a classical implementation of a singleton using ES6.

  • The instance "field" is "private". This really means we hide the instance as a property of the constructor. Somewhere not Constructor.prototype, which will be available to the instance through prototipical inheritance.
  • The constructor is "private". We really are just throwing an error when the caller is not the static getInstance method.

Also of note. It’s important to understand what the keyword this means in different contexts.

In the constructor, this points to the instance being created.

In the static getInstance method, this points to the left of the dot, Universe constructor function which, is an object like most things in JS and can hold properties.

class Universe {
    constructor() {
       if (!((new Error).stack.indexOf("Universe.getInstance") > -1)) {
           throw new Error("Constructor is private. Use static method getInstance.");  
       } 
       this.constructor.instance = this;
       this.size = 1;
    }
    static getInstance() {
        if (this.instance) {
            return this.instance;
        }
        return new this;
    }
    expand() {
        this.size *= 2;
        return this.size;
    }
}


//console.log(Universe.getInstance())
//console.log(Universe.getInstance().expand())
//console.log(Universe.getInstance())
//console.log(new Universe())
const getInstance= () => { console.log('hi'); 
    console.log("From singleton: ", Universe.getInstance()); return new Universe() }; 
console.log(getInstance())

3 Comments

This won't work because you have no control over the stack. Here's an example that bypasses your stack check; it could happen anywhere in the program: const getInstance= () =>{ console.log('hi') return new Universe() }; console.log(getInstance())
This works now. I changed the stack check to "Universe.getInstance" - which is how it is defined anyways. GetInstance now fails the check. The call stack consists of function definition names and their associated environments called one within the next so in order to beat this, we need a parent caller to be named Universe.getInstance. I suppose we can redefine Universe.getInstance = () => { return new Universe()} to bypass the singleton functionality but this would have to be pretty intentional on the developers part, to break the singleton, and not accidental.
What if another class is called MyUniverse? That will still pass your string comparison against "Universe.getInstance()". This is not a rare occurrence.
-2

Here is a more elegant solution based on class hierarchy:

class ParentClass{
    #p1=10;

    constructor(){
        this.#p1=100;
    }

    setP1(value){
        this.#p1=value;
    }

    parentMethod(){
        console.log(this.#p1);
    }
}

class ClassScore extends ParentClass {

    constructor(score){
        throw new Error('The constructor is private');
    }

    static #AsalClass = class ClassScore extends ParentClass{
        score;
        hasPassed;
        constructor(JaaliClass, score){
            super(); 
            this.score = score;
            this.hasPassed = score>39;
            this.constructor = JaaliClass;
        }
        getScore(){
            console.log('asal class');
            return this.score;
        }
    };

    static mkNewInstance = function (score) {
        return new ClassScore.#AsalClass(ClassScore, score);
    };

}

let c= ClassScore.mkNewInstance(40);
console.log(c);
console.log(c.constructor);
console.log(c.getScore());
c.parentMethod();
console.log(c instanceof ClassScore);
console.log(c instanceof ParentClass);


console.log("-------------------b");
let b= ClassScore.mkNewInstance(30);
console.log(b);

console.log("-------------------d");
let d=new c.constructor(60);
console.log(d);

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.