构造函数与工厂函数


150

有人可以澄清Javascript中的构造函数和工厂函数之间的区别。

何时使用一个代替另一个?

Answers:


149

基本区别在于,构造函数与new关键字一起使用(这会使JavaScript自动创建一个新对象,this在该函数内将该对象设置为该对象,然后返回该对象):

var objFromConstructor = new ConstructorFunction();

工厂函数的调用类似于“常规”函数:

var objFromFactory = factoryFunction();

但是要使它被视为“工厂”,就需要返回某个对象的新实例:如果它仅返回布尔值或其他内容,则不会将其称为“工厂”函数。这不会像和一样自动发生new,但在某些情况下确实提供了更大的灵活性。

在一个非常简单的示例中,上面引用的功能可能看起来像这样:

function ConstructorFunction() {
   this.someProp1 = "1";
   this.someProp2 = "2";
}
ConstructorFunction.prototype.someMethod = function() { /* whatever */ };

function factoryFunction() {
   var obj = {
      someProp1 : "1",
      someProp2 : "2",
      someMethod: function() { /* whatever */ }
   };
   // other code to manipulate obj in some way here
   return obj;
}

当然,您可以使工厂功能比该简单示例复杂得多。

工厂函数的一个优点是,根据某些参数,要返回的对象可以是几种不同的类型。


14
“(编辑:这可能是个问题,因为如果没有新功能,该功能仍会运行,但不会按预期运行)。” 仅当您尝试使用“ new”调用工厂函数或尝试使用“ this”关键字分配给实例时,这才是问题。否则,您只需创建一个新的任意对象,然后将其返回即可。没问题,只是一种不同的,更灵活的处理方式,更少的样板且不会将实例化细节泄漏到API中。
埃里克·埃利奥特

6
我想指出的是,两种情况下的示例(构造函数与工厂函数)应该是一致的。工厂函数的示例不包含someMethod工厂返回的对象,因此这里有些模糊。在工厂函数内部,如果只做一次var obj = { ... , someMethod: function() {}, ... },那将导致返回的每个对象都拥有一个不同的副本,someMethod这是我们可能不想要的。那就是在工厂函数中使用newprototype内部帮助的地方。
巴拉特·哈特里

3
正如您已经提到的,有些人尝试使用工厂函数只是因为他们不想将错误留给人们忘记使用new构造函数的地方。我认为这可能是需要了解如何用工厂函数示例替换构造函数的地方,而我认为示例之间的一致性是必需的。无论如何,答案是足够的。这只是我想提出的一点,而不是我以任何方式降低答案的质量。
巴拉特·哈特里

4
对我而言,Factory函数的最大优点是您可以获得更好的封装数据隐藏,这在某些应用程序中可能很有用。如果使每个实例属性和方法公开并容易被用户修改没有问题,那么除非您不喜欢某些人喜欢的“ new”关键字,否则我认为Constructor函数更合适。
devius 2014年

1
@Federico-工厂方法不必只返回一个普通对象。它们可以在new内部使用,也可以Object.create()用于创建具有特定原型的对象。
nnnnnn 2015年

109

使用构造函数的好处

  • 大多数书籍都教您使用构造函数和 new

  • this 指新对象

  • 有些人喜欢阅读的方式var myFoo = new Foo();

缺点

  • 实例化的细节(通过new需求)泄漏到了调用API中,因此所有调用者都与构造函数实现紧密耦合。如果您需要工厂的额外灵活性,则必须重构所有调用方(公认的例外情况,而不是规则)。

  • 忘记new是一个常见的错误,您应该强烈考虑添加样板检查,以确保正确调用了构造函数(if (!(this instanceof Foo)) { return new Foo() })。编辑:自ES6(ES2015)起,您不能忘记new使用class构造函数,否则构造函数将引发错误。

  • 如果您进行instanceof检查,则对于是否new需要进行检查会产生歧义。我认为不应该这样。您已经有效地缩短了new要求,这意味着您可以消除缺点1。但是,除了name之外,您几乎只有一个工厂函数,带有额外的样板,大写字母和较不灵活的this上下文。

构造函数违反了开放/封闭原则

但我主要担心的是,它违反了开放/封闭原则。您开始导出构造器,用户开始使用构造器,然后逐渐意识到需要工厂的灵活性,例如(将实现切换为使用对象池,或在执行上下文之间实例化,或者使用原型OO具有更大的继承灵活性)。

但是,您被困住了。如果不破坏调用构造函数的所有代码,就无法进行更改new。例如,您不能切换到使用对象池来提高性能。

另外,使用构造函数会给您一个欺骗性的效果instanceof,该欺骗性在跨执行上下文的情况下不起作用,并且如果您的构造函数原型被换出也不起作用。如果您开始返回,它也会失败this从构造函数,然后切换到导出任意对象,则必须执行此操作才能在构造函数中启用类似工厂的行为,。

使用工厂的好处

  • 更少的代码-无需样板。

  • 您可以返回任意对象,也可以使用任意原型-给您更大的灵活性来创建实现相同API的各种对象。例如,可以创建HTML5和Flash播放器实例的媒体播放器,或者可以发出DOM事件或Web套接字事件的事件库。工厂还可以跨执行上下文实例化对象,利用对象池,并允许更灵活的原型继承模型。

  • 您将不需要从工厂转换为构造函数,因此重构永远不会成为问题。

  • 使用毫不含糊new。别。(它将使this行为变差,请参阅下一点)。

  • this表现为它通常会-所以你可以用它来访问父对象(例如,内部的player.create()this指的是player,就像任何其他方法将调用callapply也重新分配this,如预期如果您存储父对象上的原型,即。可以很好地动态交换功能,并为对象实例化启用非常灵活的多态性。

  • 对于是否要大写没有歧义。别。棉绒工具会抱怨,然后您会尝试使用new,然后您将撤消上述好处。

  • 有些人喜欢阅读var myFoo = foo();var myFoo = foo.create();阅读。

缺点

  • new表现不符合预期(请参见上文)。解决方案:不要使用它。

  • this不引用新对象(相反,如果使用点符号或方括号符号调用构造函数,例如foo.bar()- this则与foo其他JavaScript方法一样引用-请参见好处)。


2
从什么意义上说,构造函数使调用者与实现紧密耦合?就构造函数参数而言,它们甚至需要传递给工厂函数,以便使用它们并在其中调用适当的构造函数。
巴拉特·哈特里

4
关于违反Open / Closed的问题:这不是全部依赖注入吗?如果A需要B,则A调用new B()或A调用BFactory.create(),它们两者都引入了耦合。另一方面,如果您在合成根目录中为A提供了B的实例,则A完全不需要了解如何实例化B。我觉得构造函数和工厂都有用途。构造函数用于简单实例化,工厂用于更复杂的实例化。但是在两种情况下,注入依赖项都是明智的。
Stefan Billiet 2014年

1
DI非常适合注入状态:配置,域对象等。这对其他所有东西来说都是过分杀伤力的。
埃里克·埃利奥特

1
大惊小怪的是,要求new违反了开放/封闭原则。参见medium.com/javascript-scene/…进行的讨论超出了这些注释所允许的范围。
埃里克·埃利奥特

3
因为任何函数都可以在JavaScript中返回一个新对象,而且很多函数都不需要使用该new关键字,所以我不认为该new关键字确实可以提供任何其他可读性。海事组织,为了使呼叫者能够键入更多内容而跳入箍圈似乎很愚蠢。
埃里克·埃利奥特


5

构造函数示例

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");
  • new创建一个原型对象,User.prototypeUser使用创建的对象作为其this值进行调用。

  • new 将其操作数的自变量表达式视为可选:

         let user = new User;

    会导致new调用User不带任何参数。

  • new返回其创建的对象,除非构造函数返回一个object value,而不是返回。这是一个边缘情况,在大多数情况下可以忽略。

利弊

构造函数创建的对象从构造函数的prototype属性继承属性,并使用instanceOf构造函数上的运算符返回true 。

如果您prototype已经在使用构造函数之后动态更改了构造函数的属性值,则上述行为可能会失败。这样做很少见,如果使用class关键字创建构造函数,则无法更改。

构造函数可以使用extends关键字扩展。

构造函数不能null作为错误值返回。由于它不是对象数据类型,因此将被忽略new

工厂功能示例

function User(name, age) {
  return {
    name,
    age,
  }
};

let user = User("Tom", 23);

这里的工厂函数不带 new。如果函数的参数及其返回的对象类型,则该函数完全负责直接或间接使用。在此示例中,它返回一个简单的[Object object],其中包含通过参数设置的某些属性。

利弊

轻松向调用者隐藏对象创建的实现复杂性。这对于浏览器中的本机代码功能特别有用。

工厂函数不必总是返回相同类型的对象,甚至可以null作为错误指示符返回。

在简单的情况下,工厂功能的结构和含义可能很简单。

返回的对象通常不会从工厂函数的prototype属性继承,而false从继承instanceOf factoryFunction

无法使用extends关键字安全地扩展工厂函数,因为扩展对象将继承自工厂函数prototype属性,而不是继承自工厂函数prototype使用的构造函数的属性。


1
这是针对同一主题针对此问题而发布的最新答案
traktor53

不仅是“ null”,而且“ new”也将忽略构造函数返回的任何前提数据类型。
维沙尔

2

工厂“总是”更好。当使用面向对象的语言时

  1. 决定合同(方法和方法)
  2. 创建公开这些方法的接口(在javascript中,您没有接口,因此您需要提出一些检查实现的方法)
  3. 创建一个工厂,该工厂返回所需的每个接口的实现。

实现(使用new创建的实际对象)不会暴露给工厂用户/消费者。这意味着只要不违反合同,工厂开发人员就可以扩展和创建新的实现方式,这使工厂用户只需从新API中受益,而不必更改其代码...如果他们使用new并出现了“ new”实现,那么他们必须去更改每条使用“ new”的行以使用“ new”实现...在工厂中,其代码不会改变...

工厂-比其他所有工厂都要好-spring框架完全基于此思想构建的。


工厂如何解决必须更换每条生产线的问题?
代号Jack

0

工厂是抽象的一层,就像所有抽象一样,它们也有一定的成本。遇到基于工厂的API时,弄清楚给定API的工厂对于API使用者可能是一个挑战。对于构造函数,可发现性是微不足道的。

在决策者和工厂之间做出决定时,您需要确定该好处是否证明了复杂性是合理的。

值得一提的是,通过返回除this或undefined以外的内容,Javascript构造函数可以是任意工厂。因此,在js中,您可以兼得两者-可发现的API和对象池/缓存。


5
在JavaScript中,使用构造函数的成本高于使用工厂的成本,因为JS中的任何函数都可以返回新对象。构造函数通过以下方法增加了复杂性:要求new,更改的行为this,更改返回值,连接原型ref,启用instanceof(存在且不应用于此目的)。表面上,所有这些都是“功能”。实际上,它们会损害您的代码质量。
埃里克·埃利奥特

0

对于差异,埃里克·埃利奥特(Eric Elliott)讲得很好,

但是对于第二个问题:

何时使用一个代替另一个?

如果您来自面向对象的背景,那么构造函数对您来说看起来更自然。这样,您就不会忘记使用new关键字。

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.