构造函数与工厂函数

Constructor function vs Factory functions

本文关键字:函数 工厂 构造函数      更新时间:2023-09-26

谁能解释一下Javascript中构造函数和工厂函数的区别?

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

基本区别是构造函数与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();的阅读方式。

<标题>缺点h1> li>

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

  • 忘记new是如此常见的错误,您应该强烈考虑添加一个样板检查,以确保正确调用构造函数(if (!(this instanceof Foo)) { return new Foo() })。编辑:从ES6 (ES2015)开始,你不能忘记newclass构造函数,否则构造函数会抛出错误。

  • 如果你做instanceof检查,它留下模糊是否需要new。在我看来,这是不应该的。您已经有效地缩短了new要求,这意味着您可以消除缺点#1。但是,在中,除了名称之外,您只是得到了一个工厂函数,带有额外的样板,大写字母和不太灵活的this上下文。

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

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

    你被困住了。如果不破坏使用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();的阅读方式。

    <标题>缺点h1> li>

    new的行为不符合预期(见上文)。解决方法:不要使用

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

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

    构造函数示例

    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忽略。

    一个Factory函数示例

    function User(name, age) {
      return {
        name,
        age,
      }
    };
    let user = User("Tom", 23);
    

    这里调用的工厂函数没有new。函数对其参数及其返回的对象类型的直接或间接使用完全负责。在这个例子中,它返回一个简单的[Object Object],并从参数中设置一些属性。

    利与弊

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

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

    在简单的情况下,工厂函数在结构和意义上可以是简单的。

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

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

    工厂"总是"更好。当使用面向对象语言时,

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

    实现(用new创建的实际对象)不公开给工厂用户/消费者。这意味着工厂开发人员可以扩展和创建新的实现,只要他/她不破坏合同……而且它允许工厂消费者从新的API中受益,而不必改变他们的代码……如果他们使用了"new",然后出现了"新"实现,那么他们必须去修改每一行使用"new"的代码,以使用"新"实现……对于工厂,它们的代码不会改变…

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

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

    当你在actor和factory之间做决定时,你需要决定它们的复杂性是否值得。

    值得注意的是,Javascript构造函数可以是任意工厂,返回的不是this或undefined。所以在js中,你可以两全其美——可发现API和对象池/缓存。

    • 我认为工厂函数优于构造函数。使用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;
      }
      

    对于差异,Eric Elliott解释得很清楚,

    但是对于第二个问题:

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

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