23

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.

musically_ut
  • 33,668
  • 8
  • 90
  • 106

4 Answers4

16

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;
})();
musically_ut
  • 33,668
  • 8
  • 90
  • 106
  • @Bergi Is there a solution which will allow me to say `x instanceof Score` otherwise? – musically_ut Feb 12 '14 at 06:25
  • 3
    @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); } ` – Mat Dec 14 '17 at 08:31
8

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 (…)
Bergi
  • 572,313
  • 128
  • 898
  • 1,281
  • Clearly a simpler solution than the one provided by @musically_ut. One that does not throw either. Btw, throwing errors is ugly… – Mat Dec 14 '17 at 08:33
  • Is this still the best way to achieve this in 2020? – Neutrino Jun 28 '20 at 11:15
  • @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. – Bergi Jun 28 '20 at 12:05
  • 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 Jun 28 '20 at 13:15
  • @Neutrino If you're still learning JavaScript, I would recommend to simply not try making constructors private. – Bergi Jun 28 '20 at 13:17
  • It's not for production code, just working through some exercises in the book 'Eloquent Javascript'. Great book btw. – Neutrino Jun 28 '20 at 13:29
7

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;
    }
};
user2864740
  • 57,407
  • 13
  • 129
  • 202
  • 1
    Just put `Score.prototype = {}` instead of shadowing the inherited `constructor` property… – Bergi Feb 10 '14 at 01:17
  • 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 Feb 12 '14 at 06:24
  • @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 ;-) – user2864740 Feb 12 '14 at 06:28
  • 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`. – musically_ut Feb 12 '14 at 07:26
0

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
}
esp
  • 7,537
  • 5
  • 46
  • 73