TL;DR:
Factory functions and constructor functions both solve the problem of object creation and code repetition, but they do it differently. Factory functions return objects explicitly and offer better flexibility and encapsulation (closures), while constructor functions use the new keyword and automatically set up prototypal inheritance, sharing methods across instances. In this post, we break down real-world examples, when to use each one, and why understanding both patterns makes your JavaScript code more powerful, scalable, and readable.


Introduction

In the previous post, we explored factory functions and constructor functions separately, learning how each helps eliminate code duplication.
Today, as promised, we’ll dive straight into a practical comparison between them — focusing almost entirely on code examples.
We’ll revisit our pet shop, explore both patterns side by side, and finally answer the million-dollar question:

👉 Which one should you use, and when?

If you enjoy learning by doing (and breaking things down step-by-step), this post was made for you.

Quick Recap

In this topic, we’ll take a much more practical approach. Almost everything will be explained using code. So, to start, let’s quickly recap what factory and constructor functions look like:

function carFactory(m) {
  return {
    model: m
  };
}

function Car(m) {
  // this = {}
  this.model = m;
  // return this
} // The commented code happens under the hood; you don't need to write it

const car1 = carFactory("McLaren");
const car2 = new Car("Sauber");

Factory Functions: Explicitly return an object with the parameters we pass. Invoked normally.

Constructor Functions: Invoked using the reserved word new, implicitly return the this = {} object. This action is called Object Instantiation. The object in question is an Instance of the constructor function.

The similarities are easy to see — both create a person object (or car in this case) and return it somehow.

But let’s focus on the differences. First, we’ll discuss factory functions, then constructor functions.


Factory Functions

function carFactory(m) {
  return {
    model: m,
    accelerate() {
      return `Accelerating a ${this.model}`;
    }
  };
}

const car1 = carFactory("McLaren");
const car2 = carFactory("Sauber");

console.log(car1);
/*
Object {
  model: "McLaren"
  accelerate: function accelerate()
  __proto__: Object {...}
}
*/

console.log(car2);
/*
Object {
  model: "Sauber"
  accelerate: function accelerate()
  __proto__: Object {...}
}
*/

car1.accelerate(); // Accelerating a McLaren
car2.accelerate(); // Accelerating a Sauber

Notice something important: where the accelerate() function is located.

It’s inside the object, alongside the model attribute.

At first glance, this doesn’t seem to matter much — both objects execute the function just fine.

But what happens if I modify that function?

car1.accelerate = function() {
  return `Accelerating a ${this.model} with all my strength!`;
};

car1.accelerate(); // Accelerating a McLaren with all my strength!
car2.accelerate(); // Accelerating a Sauber

Notice how only car1’s function changed. This tells us a few things:

  1. car1.accelerate() and car2.accelerate() are not the same function in memory.

    Every time carFactory() creates an object, it copies everything from inside the return into a new object. Each object gets its own copy of the function and attribute.

  2. Since each object has its own copy, it consumes more memory.

    With simple functions like this, it’s not a big deal. But at scale, memory consumption adds up.

  3. Finally, it means that if I want to modify the function logic, I would need to modify it in every created object.

Conclusion:

Even though we’re creating objects and "inheriting" characteristics defined in the factory function, we’re not building an inheritance tree or hierarchy — we are merely copying characteristics.

Is that a bad thing? Not necessarily!

It has its advantages. But before we understand the advantages, let's look at the ways of solving it. First, the bad way (please, don't ever do this — seriously. I'm only showing it for didactic reasons).


Solution 1 — Terrible

If we look again at car1 and car2, we’ll see they are plain Objects. Meaning essentially they are pure instances of Object.

In theory, we could add a function to any __proto__ or prototype (some people already know where this is going, and they are not liking it at all).

car1.__proto__.floorIt = function() {
  return "Vrooooommmm";
};

car1.floorIt(); // Vrooooommmm
car2.floorIt(); // Vrooooommmm

Solved, right?

Yes — and now you’ve created a monster, a catastrophe.

Object.prototype === car1.__proto__; // true

See the problem?

If not, here it is:

const totallyNotACar = {};

totallyNotACar.floorIt(); // Vrooooommmm

Vrooooommmm.

Now everything can floor it. Seriously — everything:

window.floorIt(); // Vrooooommmm
const numbers = [1, 2, 3, 4];
numbers.floorIt(); // Vrooooommmm

Why does this happen?

Remember from the inheritance topic? Everything in JS is ultimately an Object.

And car1 was a pure Object — so modifying its prototype modified every Object’s prototype, and now everything can go Vrooooommmm


Solution 2 — Bad

The second solution is to create a custom __proto__ before building the factory function, and then assign it to every object created.

const prototypeCarFactory = {
  accelerate() {
    return `Accelerating a ${this.model} with all my strength!`;
  }
};

function carFactory(m) {
  return Object.create(prototypeCarFactory, {
    model: {
      value: m
    }
  });
}

const car1 = carFactory("McLaren");
const car2 = carFactory("Sauber");

car1.accelerate(); // Accelerating a McLaren with all my strength!
car2.accelerate(); // Accelerating a Sauber with all my strength!

console.log(car1);
/*
Object {
  model: "McLaren"
  __proto__: Object {...}
}
*/

console.log(car2);
/*
Object {
  model: "Sauber"
  __proto__: Object {...}
}
*/

console.log(car2.__proto__);
/*
Object {
  accelerate: function accelerate
  __proto__: Object {...}
}
*/

Now the accelerate() function is no longer inside the object itself, but inside its __proto__.

And __proto__ does not point directly to Object anymore.

If I modify car1.__proto__, it will reflect on car2.__proto__ — without affecting every object in the system.


Why are both of these solutions bad?

Because the beauty of factory functions is precisely in not having inheritance.

They favor simplicity and composition instead.

Now, let’s move on to constructor functions.


Constructor Functions

function Car(m) {
  this.model = m;
}

const car1 = new Car("McLaren");

console.log(car1);
/*
Object {
  model: "McLaren"
  __proto__: {
    constructor: function Car(m)
  }
}
*/

The created object is an instance of Car(), meaning its prototype is Car, not Object.

It directly inherits from Car.

In other words, all that messy workaround we needed in the factory to avoid pointing __proto__ to Object?

Here, it’s done automatically.

Every object already comes with its inheritance correctly set by default.


"Alright Matheus, so I should always use constructors, since they do everything factories do, and more?"

Hold your horses, young padawan.

Like I said before: each approach has its own advantages.


When Should You Use Which?

The choice between Factory Functions and Constructor Functions depends on what you’re aiming for in terms of inheritance, encapsulation, readability, flexibility, and even performance.

Let’s understand each case:


Use Factory Functions when...

1. You want encapsulation with closures

Factory functions allow you to create truly private variables using closures (if you don't know what they are, don't worry, we'll get there in the next topics), something constructors can't do easily (without hacks like Symbols or #private fields in ES2022+).

function bankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
    },
    withdraw(amount) {
      if (amount <= balance) balance -= amount;
    },
    currentBalance() {
      return balance;
    }
  };
}

const account = bankAccount(100);
account.deposit(50);
console.log(account.currentBalance()); // 150
console.log(account.balance); // undefined — balance is protected

2. You need more flexibility

You can easily return different kinds of objects, apply conditional logic, or change the object depending on parameters.

This allows for more dynamic object creation.

function createAnimal(type) {
  if (type === 'dog') {
    return { bark: () => 'Woof!' };
  } else if (type === 'cat') {
    return { meow: () => 'Meow!' };
  }
}

const dog = createAnimal('dog');
console.log(dog.bark()); // Woof!

3. You don't need an explicit inheritance chain

Factory functions create pure, independent objects.

This is perfect in simpler systems or more functional programming styles, where inheritance would just add unnecessary complexity.

It’s very similar to how structs work in C++.


4. You prefer composition over inheritance

Factory functions are excellent for object composition.

Instead of relying on a rigid inheritance structure, you combine behaviors.

This follows the principle: "Composition over inheritance".

const canFly = (name) => ({
  fly: () => `${name} is flying!`
});

const canSwim = (name) => ({
  swim: () => `${name} is swimming!`
});

function createDuck(name) {
  return {
    name,
    ...canFly(name),
    ...canSwim(name)
  };
}

const donald = createDuck("Donald");
donald.fly(); // Donald is flying!
donald.swim(); // Donald is swimming!

Use Constructor Functions when...

1. You want instances with prototypes and inheritance

Constructor functions automatically give you a prototype chain.

And if you want methods to be shared among all instances (rather than duplicated), constructors shine.

function Person(name) {
  this.name = name;
}

Person.prototype.speak = function() {
  return `Hi, my name is ${this.name}`;
};

const john = new Person("John");
console.log(john.speak()); // Hi, my name is John

2. You care about performance at large scale

Since methods live on the prototype, they are not copied for each new instance, saving memory.

// Every person shares the same method
console.log(john.speak === new Person("Maria").speak); // true

3. You are using classes or frameworks based on OOP

Some libraries (like older React with createClass, or certain prototype-oriented libraries) expect you to use constructors or classes.

Also, developers coming from Java, C#, or other OOP backgrounds will find this more natural.


Summary:

  • If you're working with simple objects without needing inheritance, and you want control or encapsulationUse Factory Functions.
  • If you're building complex types that share many behaviors and you want memory efficiencyUse Constructor Functions.

Example

Remember the PetShop from the previous topic?

Back then, we built an HTML page that registered Pets and created petCards.

Over time, we noticed that Pets became more and more complex, needing to inherit shared behaviors.

But the Cards?

They remained simple — we just needed a small, standardized object to insert into the DOM.

What does this show us?

That in this case, it would be ideal to have Pets created with a constructor function and petCards created with a factory function.

</span>
 lang="en">

   charset="UTF-8">
  Pet Registration
  
    body {
      font-family: sans-serif;
      padding: 20px;
      background-color: #f7f7f7;
    }

    form {
      margin-bottom: 20px;
      padding: 16px;
      background: white;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      max-width: 400px;
    }

    label {
      display: block;
      margin-bottom: 8px;
      font-weight: bold;
    }

    input {
      width: 100%;
      padding: 8px;
      margin-bottom: 16px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    button {
      padding: 10px 16px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 6px;
      cursor: pointer;
    }

    .card-pet {
      background: white;
      border: 2px solid #ccc;
      border-radius: 8px;
      padding: 12px;
      margin-bottom: 10px;
      max-width: 280px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
  



  Pet Registration

   id="form-pet">
     for="petName">Pet Name
     type="text" id="petName" required>

     for="ownerName">Owner's Name
     type="text" id="ownerName" required>

     for="species">Species
     type="text" id="species" required>

     type="submit">Register Pet
  

   id="area-pets">

  
    function Pet(petName, ownerName, species) {
      let _petName = petName;
      let _ownerName = ownerName;
      let _species = species;

      this.identify = function() {
        return `The ${_species} ${_petName} belongs to ${_ownerName}`;
      };

      this.getData = function() {
        return { petName: _petName, ownerName: _ownerName, species: _species };
      };
    }

    function petCardFactory(pet) {
      const { petName, ownerName, species } = pet.getData();

      const card = document.createElement("div");
      card.className = "card-pet";

      const title = document.createElement("h3");
      title.textContent = petName;

      const speciesEl = document.createElement("p");
      speciesEl.textContent = `Species: ${species}`;

      const ownerEl = document.createElement("p");
      ownerEl.textContent = `Owner: ${ownerName}`;

      card.appendChild(title);
      card.appendChild(speciesEl);
      card.appendChild(ownerEl);

      return card;
    }

    // Form logic
    const form = document.getElementById("form-pet");

    form.addEventListener("submit", function(event) {
      event.preventDefault();

      const petName = document.getElementById("petName").value;
      const ownerName = document.getElementById("ownerName").value;
      const species = document.getElementById("species").value;

      const newPet = new Pet(petName, ownerName, species);
      document.getElementById("area-pets").appendChild(petCardFactory(newPet));

      form.reset();
    });

  





    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  Bonus: Class vs Constructor
ES6 introduced the class keyword in JavaScript.
I'll be straight to the point here, without too much sugarcoating:  Classes are EXACTLY the same thing as constructor functions.
They just use a cleaner syntax — but underneath, they still generate constructor functions.
So why didn’t we use class from the start?
Because I wanted you to understand the concept first — before seeing the syntax sugar.From now on, whenever we use constructors, we’ll define them using the class syntax:

class Car {
  constructor(m) {
    this.model = m;
  }

  accelerate() {
    return `Accelerating a ${this.model}`;
  }
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    




But be warned: there’s a lot more to classes in JavaScript (especially around inheritance, super, getters/setters, static properties, etc).
We’ll explore those topics later on!
  
  
  Wrapping up
Both factory functions and constructor functions are powerful tools in a JavaScript developer’s toolkit.
Factory functions give you encapsulation, flexibility, and favor composition over inheritance.
Constructor functions (and classes) give you automatic inheritance, prototype efficiency, and a traditional OOP structure.
Choosing between them depends on your project's needs — whether you prioritize simplicity and modularity, or need robust hierarchies and performance across many instances.Understanding both approaches makes you a more complete developer, ready to pick the right tool for the right job.💬 What about you? Do you lean more toward factories, constructors, or mix both depending on the case?
Drop a comment — I’d love to hear how you apply this in your projects!🚀 Next up: We'll explore what exactly this means in JavaScript — a topic that still trips up even experienced devs!📬 And don’t forget: follow me @matheusjulidori for more hands-on JavaScript tips, patterns, and dev-friendly content, every week! Share this content with your peers if you enjoyed it!