构造函数与工厂函数
Posted
技术标签:
【中文标题】构造函数与工厂函数【英文标题】:Constructor function vs Factory functions 【发布时间】:2012-01-31 16:38:30 【问题描述】:谁能解释一下javascript中构造函数和工厂函数之间的区别。
什么时候用一个代替另一个?
【问题讨论】:
【参考方案1】:为之前的答案添加的一个新内容是,有一种声明构造函数的新方法。
class CreateCirle
constructor(radius)
this.radius = radius;
this.draw = function ()
console.log('we draw a circle with radius: ', this.radius);
;
this.name = "Circly";
this.address = "Math Graph";
您必须添加 class
和 constructor
关键字。
然后正常调用它,没有区别。
let cirle = new CreateCirle(5);
cirle.draw();
不要忘记使用前面提到的 PascalCase 命名约定。
【讨论】:
【参考方案2】:基本区别在于构造函数与 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;
当然,你可以让工厂函数比那个简单的例子复杂得多。
工厂函数的一个优点是当要返回的对象可以是几种不同的类型,具体取决于某些参数。
【讨论】:
"(编辑:这可能是个问题,因为没有 new 函数仍然会运行,但不会像预期的那样)。"如果您尝试使用“new”调用工厂函数或尝试使用“this”关键字分配给实例,这只是一个问题。否则,您只需创建一个新的任意对象,然后返回它。没问题,只是一种不同的、更灵活的做事方式,样板更少,并且不会将实例化细节泄漏到 API 中。 我想指出这两种情况(构造函数与工厂函数)的示例应该是一致的。工厂函数的示例不包括工厂返回的对象的someMethod
,这就是它变得有点模糊的地方。在工厂函数内部,如果只执行var obj = ... , someMethod: function() , ...
,这将导致每个返回的对象都持有someMethod
的不同副本,这可能是我们不想要的。这就是在工厂函数中使用new
和prototype
会有帮助的地方。
正如你已经提到的,有些人尝试使用工厂函数只是因为他们不打算留下人们忘记使用 new
和构造函数的错误;我认为这是一个可能需要查看如何用工厂函数替换构造函数示例的地方,这就是我认为需要示例中的一致性的地方。无论如何,答案足够丰富。这只是我想提出的一点,并不是我要以任何方式降低答案的质量。
对我来说,Factory 函数的最大优势是您可以获得更好的封装和数据隐藏,这在某些应用程序中可能很有用。如果让每个实例属性和方法都公开并且用户可以轻松修改没有问题,那么我猜 Constructor 函数更合适,除非你像某些人那样不喜欢“new”关键字。
@Federico - 工厂方法不必只返回一个普通对象。他们可以在内部使用new
,也可以使用Object.create()
创建具有特定原型的对象。【参考方案3】:
构造函数示例
function User(name)
this.name = name;
this.isAdmin = false;
let user = new User("Jack");
new
在User.prototype
上创建一个原型对象并调用User
,并将创建的对象作为其this
值。
new
将其操作数的参数表达式视为可选:
let user = new User;
将导致 new
不带参数地调用 User
。
new
返回它创建的对象,除非构造函数返回一个对象值,否则它会被返回。这是一个边缘情况,大部分情况下可以忽略。
优点和缺点
由构造函数创建的对象从构造函数的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
属性,而是从instanceOf factoryFunction
返回false
。
无法使用 extends
关键字安全地扩展工厂函数,因为扩展对象将从工厂函数 prototype
属性继承,而不是从工厂函数使用的构造函数的 prototype
属性继承。
【讨论】:
这是针对同一主题的 this question 的回复发布的较晚答案, 不只是“null”,“new”也会忽略构造函数返回的任何前置数据类型。【参考方案4】:对于差异,Eric Elliott 说得很好,
但是对于第二个问题:
什么时候用一个代替另一个?
如果您来自面向对象的背景,构造函数对您来说看起来更自然。
这样你不应该忘记使用new
关键字。
【讨论】:
【参考方案5】:使用构造函数的好处
大部分书籍教你使用构造函数和new
this
指的是新对象
有些人喜欢var myFoo = new Foo();
的阅读方式。
缺点
实例化的细节被泄露到调用 API 中(通过 new
要求),因此所有调用者都与构造函数实现紧密耦合。如果您需要工厂的额外灵活性,则必须重构所有调用者(诚然是例外情况,而不是规则)。
忘记new
是一个常见的错误,您应该强烈考虑添加样板检查以确保正确调用构造函数(if (!(this instanceof Foo)) return new Foo()
)。编辑:因为 ES6 (ES2015) 你不能忘记 new
和 class
构造函数,否则构造函数会抛出错误。
如果您进行instanceof
检查,则对于是否需要new
会产生歧义。在我看来,不应该。您已经有效地缩短了new
要求,这意味着您可以消除缺陷#1。但是除了名称之外,你只是得到了一个工厂函数,还有额外的样板、大写字母和不太灵活的this
上下文。
构造函数打破了开放/封闭原则
但我主要担心的是它违反了开放/封闭原则。您开始导出构造函数,用户开始使用构造函数,然后您意识到您需要工厂的灵活性,而不是(例如,将实现切换为使用对象池,或跨执行上下文进行实例化,或使用原型 OO 具有更大的继承灵活性)。
不过,你被困住了。如果不破坏使用new
调用构造函数的所有代码,则无法进行更改。例如,您不能切换到使用对象池来提高性能。
另外,使用构造函数会给你一个欺骗性的instanceof
,它在执行上下文中不起作用,并且如果你的构造函数原型被换出,它也不起作用。如果您开始从构造函数返回 this
,然后切换到导出任意对象,它也会失败,您必须这样做才能在构造函数中启用类似工厂的行为。
使用工厂的好处
更少的代码 - 不需要样板。
您可以返回任意对象,并使用任意原型 - 让您更灵活地创建实现相同 API 的各种类型的对象。例如,可以创建 html5 和 Flash 播放器实例的媒体播放器,或者可以发出 DOM 事件或 Web 套接字事件的事件库。工厂还可以跨执行上下文实例化对象,利用对象池,并允许更灵活的原型继承模型。
您永远不需要从工厂转换为构造函数,因此重构永远不会成为问题。
关于使用new
没有歧义。别。 (这会使this
表现不佳,见下一点)。
this
的行为与平常一样 - 因此您可以使用它来访问父对象(例如,在 player.create()
内部,this
指的是 player
,就像任何其他方法调用一样. call
和 apply
也按预期重新分配 this
。如果您将原型存储在父对象上,这可能是动态交换功能的好方法,并为您的对象实例化启用非常灵活的多态性。
关于是否大写没有歧义。别。 Lint 工具会报错,然后你会想尝试使用new
,然后你就会取消上述好处。
有些人喜欢var myFoo = foo();
或var myFoo = foo.create();
的阅读方式。
缺点
new
的行为不符合预期(见上文)。解决方法:不要使用。
this
不引用新对象(相反,如果使用点表示法或方括号表示法调用构造函数,例如 foo.bar() - this
指的是 foo
- 只是像所有其他 JavaScript 方法一样——查看好处)。
【讨论】:
在什么意义上,构造函数使调用者与其实现紧密耦合?就构造函数参数而言,它们甚至需要传递给工厂函数才能使用它们并在其中调用适当的构造函数。 关于违反Open/Closed:这不就是依赖注入吗?如果 A 需要 B,无论 A 调用 new B() 还是 A 调用 BFactory.create(),它们都引入了耦合。另一方面,如果您在组合根中为 A 提供 B 的实例,则 A 根本不需要知道 B 是如何实例化的。我觉得建设者和工厂都有其用途;构造函数用于简单实例化,工厂用于更复杂的实例化。但在这两种情况下,注入你的依赖是明智的。 DI 适合注入状态:配置、域对象等。这对其他一切来说都是矫枉过正。 大惊小怪的是,要求new
违反了开放/封闭原则。请参阅 medium.com/javascript-scene/… 以获得比这些 cmets 允许的更大的讨论。
因为任何函数都可以在 JavaScript 中返回一个新对象,而且很多函数在没有 new
关键字的情况下这样做,我不相信 new
关键字实际上确实提供了任何额外的可读性。 IMO,为了让调用者能够输入更多内容而跳槽似乎很愚蠢。【参考方案6】:
工厂“总是”更好。当使用面向对象的语言时,那么
-
决定合同(方法和他们将做什么)
创建公开这些方法的接口(在 javascript 中您没有接口,因此您需要想出一些方法来检查实现)
创建一个工厂,返回所需的每个接口的实现。
实现(使用 new 创建的实际对象)不会暴露给工厂用户/消费者。这意味着工厂开发人员可以扩展和创建新的实现,只要他/她不违反合同......它允许工厂消费者从新的 API 中受益,而无需更改他们的代码......如果他们使用了新的并且出现了“新”实现,那么他们必须去更改使用“新”的每一行以使用“新”实现......与工厂他们的代码不会改变......
工厂 - 比其他任何东西都好 - spring 框架完全围绕这个想法构建。
【讨论】:
工厂如何解决每条线都要换线的问题?【参考方案7】:工厂是一个抽象层,并且像所有抽象一样,它们在复杂性方面具有成本。当遇到基于工厂的 API 时,弄清楚给定 API 的工厂是什么对于 API 使用者来说是一个挑战。对于构造函数,可发现性是微不足道的。
在 ctor 和 factory 之间做出决定时,您需要确定其复杂性是否符合利益。
值得注意的是,Javascript 构造函数可以是任意工厂,方法是返回 this 或 undefined 以外的内容。因此,在 js 中,您可以获得两全其美的优势 - 可发现的 API 和对象池/缓存。
【讨论】:
在 JavaScript 中,使用构造函数的成本高于使用工厂的成本,因为 JS 中的任何函数都可以返回一个新对象。构造函数通过以下方式增加复杂性:要求new
、更改this
的行为、更改返回值、连接原型引用、启用instanceof
(它存在且不应用于此目的)。从表面上看,所有这些都是“功能”。实际上,它们会损害您的代码质量。【参考方案8】:
构造函数返回您调用它的类的实例。工厂函数可以返回任何东西。当您需要返回任意值或类具有大型设置过程时,您将使用工厂函数。
【讨论】:
以上是关于构造函数与工厂函数的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript 创建对象 (工厂模式构造函数模式原型模式组合使用构造函数模式与原型模式)