构造函数与工厂函数

有人能解释一下 Javascript 中构造函数和工厂函数之间的区别吗。

什么时候用一个代替另一个?

74146 次浏览

构造函数返回调用它的类的实例。工厂函数可以返回任何内容。当需要返回任意值或者类有一个大的设置过程时,可以使用工厂函数。

基本区别在于构造函数与 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

  • this引用新对象

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

缺点

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

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

  • 如果您进行 instanceof检查,那么在是否需要 new这个问题上就会留下模棱两可的地方。在我看来,不应该是这样的。您已经有效地绕过了 new的要求,这意味着您可以消除缺点 # 1。但是,然后 你只有一个名义上的工厂函数,与额外的样板,一个大写字母,和不灵活的 this上下文。

构造函数打破了开/闭原则

但我主要担心的是它违反了开放/封闭原则。你开始导出一个构造函数,用户开始使用构造函数,然后你意识到你需要工厂的灵活性,而不是(例如,切换实现使用对象池,或跨执行上下文实例化,或使用原型面向对象具有更多的继承灵活性)。

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

而且,使用构造函数会给你一个欺骗性的 instanceof,它不能在执行上下文中工作,如果你的构造函数原型被交换掉了,它也不能工作。如果从构造函数开始返回 this,然后切换到导出任意对象,那么它也会失败,您必须这样做才能在构造函数中启用类似工厂的行为。

使用工厂的好处

  • 减少代码-不需要样板。

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

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

  • 使用 new不要含糊其辞。不要。(这会使 this表现糟糕,见下一点)。

  • this的行为与正常情况一样——因此您可以使用它来访问父对象(例如,在 player.create()中,this引用 player,就像任何其他方法调用一样。callapply也按照预期重新分配 this。如果您将原型存储在父对象上,这可能是一种动态交换功能的好方法,并为您的对象实例化启用非常灵活的多态性。

  • 对于是否投资毫不含糊。不要。Lint 工具会发出抱怨,然后您将尝试使用 new,然后您将取消上面描述的好处。

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

缺点

  • new的行为与预期不符(见上文)。解决方案: 不要使用它。

  • this并不引用新对象(相反,如果构造函数是用点表示法或方括号表示法调用的,例如 foo.bar ()-this引用 foo-就像其他所有 JavaScript 方法一样——参见优点)。

工厂是一个抽象层,和所有抽象一样,它们在复杂性上有一定的代价。当遇到基于工厂的 API 时,要弄清楚给定 API 的工厂是什么对于 API 使用者来说是一个挑战。构造函数的可发现性是微不足道的。

在决定转换器和工厂之间的时候,你需要决定复杂性是否是由利益决定的。

值得注意的是,Javascript 构造函数可以是任意的工厂,只要返回除此之外的内容或未定义的内容即可。因此,在 js 中,您可以同时获得可发现的 API 和对象池/缓存。

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

  1. 决定合同(方法和他们将要做什么)
  2. 创建公开这些方法的接口(在 javascript 中你没有接口,所以你需要想出一些方法来检查实现)
  3. 创建一个返回所需每个接口的实现的工厂。

实现(使用 new 创建的实际对象)不向工厂用户/使用者公开。这意味着工厂开发人员可以扩展和创建新的实现,只要他/她没有违反合同... 它允许工厂消费者只是受益于新的 API,而不必改变他们的代码... 如果他们使用新的和一个“新的”实现出现,那么他们必须去和改变每一行使用“新的”使用“新的”实现... 与工厂他们的代码不改变..。

工厂——比其他任何东西都好—— Spring 框架完全是围绕这个想法构建的。

一个构造函数示例

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


let user = new User("Jack");
  • newUser.prototype上创建一个原型化的对象,并使用创建的对象作为其 this值调用 User

  • 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 对象] ,其中包含从参数中设置的一些属性。

利与弊

很容易向调用者隐藏对象创建的实现复杂性。这对于浏览器中的本机代码函数特别有用。

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

在简单的情况下,工厂函数的结构和含义可以很简单。

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

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

对于这些差异,埃里克 · 埃利奥特解释得很清楚,

但是对于第二个问题:

什么时候用一个代替另一个?

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

  • 我认为工厂函数优于构造函数。使用带有构造函数的 new,我们将代码绑定到创建对象的一种特定方式,而使用工厂,我们是自由的,因此我们可以创建更多不同的实例,而无需绑定我们自己。假设我们有这门课:

    Const file = new CreateFile (name)

如果我们想要重构 CreateFile 类,为我们的服务器支持的文件格式创建子类,我们可以编写一个优雅的工厂函数:

function CreateFile(name) {
if (name.match(/\.pdf$/)) {
return new FilePdf(name);
} else if (name.match(/\.txt$/)) {
return new FileTxt(name);
} else if (name.match(/\.md$/)) {
return new FileMd(name);
} else {
throw new Error("Not supprted file type");
}
}
  • 使用工厂函数,我们可以实现私有变量,对用户隐藏信息,这就是所谓的 encapsulation

    function createPerson(name) {
    const privateInfo = {};
    // we create person object
    const person = {
    setName(name) {
    if (!name) {
    throw new Error("A person must have a name");
    }
    privateInfo.name = name;
    },
    getName() {
    return privateInfo.name;
    },
    };
    person.setName(name);
    return person;
    }