如何在JavaScript中定义私有构造函数?

30

我已经在JS中定义了纯对象,这些对象公开了某些静态方法,应该使用这些方法来构建它们,而不是使用构造函数。如何在Javascript中将我的类的构造函数设为私有的?

我如何在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;
})();

更新:解决方案仍应允许我测试x instanceof Score。否则,@user2864740提供的仅公开静态构造函数的解决方案可行。


我建议你阅读http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript。 - Arnaldo Ignacio Gaspar Véjar
1
如果您使用 class#constructor,则会出现 SyntaxError: Class constructor may not be a private method(v8)和 SyntaxError: bad method definition(SpiderMonkey)。因此,我认为私有构造函数不应该是 JS/ES 的一部分。如果您想创建一个单例,可以创建一个私有字段 static #instance = null,在构造函数中 if (YourClass.#instance instanceof YourClass) return YourClass.#instance // or throw error perhaps - undefined
7个回答

20

一个变量(initializing)可以在闭包中使用,如果构造函数直接调用而不是通过类方法调用,则可能会抛出错误:

在闭包中可以使用一个变量(initializing), 如果构造函数被直接调用而不是通过类方法调用,则会产生错误:

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

@Bergi 有没有其他解决方案可以让我说“x instanceof Score”呢? - musically_ut
3
@Bergi,可以通过提供一个优雅的回退来避免抛出异常: 如果(!initializing){ console.warn('使用了私有构造函数(请修复此错误)'); return Score.mkNewScore(score); } - Mat

9
有一种解决方案可以让我使用 x instanceof Score 吗?
是的。从概念上讲,@user2864740 是正确的,但为了让 instanceof 正常工作,我们需要暴露(return)一个函数而不是普通对象。如果该函数有与我们内部私有构造函数相同的 .prototype,那么 instanceof 运算符 就会按预期工作:
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 (…)

显然,这比@musically_ut提供的解决方案更简单。它不会抛出任何异常。顺便说一句,抛出错误很丑陋... - Mat
在2020年,这仍然是实现这个目标的最佳方式吗? - Neutrino
@Neutrino 这仍然可以工作,但你可能想使用 class 语法。今天,我会传递一个 const token = Symbol() 作为额外的构造函数参数,在检查失败时抛出异常,并将 token 限定于只有那些应该获得访问权限的函数。 - Bergi
我认为基于类语法的方法现在更加适合,但我仍在学习JavaScript,坦白地说,我很难理解其中的一些内容。你能否更新你的答案并提供一个快速示例? - Neutrino
@Neutrino 如果你还在学习JavaScript,我建议不要尝试将构造函数设为私有。 - Bergi
这不是用于生产代码的,只是在练习《JavaScript精粹》一书中的一些例子。顺便说一下,这是一本很棒的书。 - Neutrino

9
为了在JS中创建私有构造函数,我喜欢创建一个只能在类(函数)文件中访问的私有键,并提供一个静态工厂函数作为构建该类的唯一允许方式。

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


7

简单来说,不要暴露构造函数。原始代码的核心问题是“静态方法”被定义为构造函数(用作“类”的属性),而不是模块的属性。

请考虑以下内容:

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

构造函数仍然可以通过.constructor访问,但是..这没什么用。此时,最好就让“聪明的用户”有权访问。

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

1
只需将 Score.prototype = {} 放置在继承的 constructor 属性之前即可,无需覆盖它。 - Bergi
这不允许我使用 x instanceof Score。有没有解决方法?如果没有,那么我可以将这个问题标记为未解决吗?对于过早接受它,我感到抱歉。:( - musically_ut
@musically_ut 使用 instanceof 需要 访问构造函数。我很少使用 instanceof,例如,在过去的一年中没有使用,而是依赖于 JavaScript 中的鸭子类型。 (随意更改您的答案,这不是 SO 的结局;-) - user2864740
这种差异对我来说变得更加重要,因为我正在使用 typescript,它缺乏联合类型。确保某些操作“安全”的唯一方法是使用 instanceof - musically_ut

0
另一种可能的简单方法是使用谓词函数而不是 instanceof。对于 TypeScript,它可以是类型保护和类型同义词,而不是导出类:
// class is private
class _Score {
  constructor() {}
}

export type Score = _Score

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

-1

公平地说,最简单的答案通常是最好的。对象字面量始终是单个实例。除了按需分配内存之外,没有太多理由使用更复杂的东西。

话虽如此,这里有一个使用ES6的经典单例实现。

  • 实例“字段”是“私有的”。这实际上意味着我们将实例隐藏为构造函数的属性。不在Constructor.prototype中,这将通过原型继承对实例可用。
  • 构造函数是“私有的”。当调用者不是静态getInstance方法时,我们确实只是抛出错误。

还值得注意的是,重要的是要理解关键字this在不同上下文中的含义。

在构造函数中,this指向正在创建的实例。

在静态getInstance方法中,this指向点左侧的Universe构造函数,它像JS中的大多数东西一样是一个对象,并且可以保存属性。

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


这是行不通的,因为您无法控制堆栈。以下是一个绕过堆栈检查的示例;它可能发生在程序的任何地方:const getInstance= () =>{ console.log('hi') return new Universe() }; console.log(getInstance()) - lwdthe1
这个现在可行了。我把栈检查改成了“Universe.getInstance”- 这本来就是它的定义方式。GetInstance 现在无法通过检查。调用栈由函数定义名称及其关联环境组成,一个接一个地调用,因此为了打破这个限制,我们需要将父调用者命名为 Universe.getInstance。我想我们可以重新定义 Universe.getInstance =()=> {return new Universe()} 来绕过单例功能,但这必须是开发人员故意为之,而不是意外情况下破坏单例。 - vrspiration
如果另一个类被称为MyUniverse,那么它仍将通过对“Universe.getInstance()”进行的字符串比较。这并不是罕见的情况。 - lwdthe1

-2

这里是基于类层次结构的更优雅的解决方案:

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


你的回答可以通过提供更多支持性信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的回答是否正确。你可以在帮助中心找到关于如何撰写好的回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接