TL;DR:
Constructor functions in JavaScript are another way to create reusable object templates, much like factory functions. They use thenew
keyword andthis
to construct and return a new object automatically — no need for an explicitreturn
. They help eliminate code duplication and improve consistency in your codebase. In this post, we revisit the pet shop example and compare constructor functions to factory functions to understand when and why to use them.
📌 Haven’t read Episode 3 yet?
Check out “Factory Functions — Do You Know How It Works?”
Introduction
If you’ve been following the series “Do You Know How It Works?”, you’ve seen that avoiding repetitive code is a major principle in writing clean JavaScript. Last week, we tackled factory functions as a way to solve that problem — and they worked beautifully for our pet shop scenario.
This week, we’re exploring another powerful (and older) JavaScript pattern: the constructor function. It looks a bit different, but at its core, it has the same mission — building reusable logic to avoid repetition. Constructor functions were the go-to before ES6 classes, and they’re still a foundational concept in the JavaScript ecosystem today.
Here is the full translation of your post, including the code, with no modifications to the original structure. Suggestions will follow below if needed.
Very similar to factory functions, a constructor function is also used to shape and build an object. They work a bit differently, but they follow the same core principle: eliminate code repetition.
In object-oriented languages, these functions are embedded within classes. They define how the class object will be built. But since JavaScript didn’t support classes for a long time and needed the same functionality, that’s where the name comes from. So, in a way, JS had constructors even before it had classes.
When we talked about factory functions, we used the pet shop example (If you haven't seen this post, read it first, we’ll omit some concepts here that were explained in it). To refresh your memory, we created a scenario where I want to register pets in my pet shop, along with information about their owners.
const pet1 = {
name: "Katze",
ownerName: "Matheus",
identify() {
return `${this.name} belongs to ${this.ownerName}`
}
}
const pet2 = {
name: "Hund",
ownerName: "Pedro",
identify() {
return `${this.name} belongs to ${this.ownerName}`
}
}
Every time I want to create an object, I’ll have to duplicate the code and the identify
function. If you’re not already tired of hearing me say that duplicating code is horrible, go read the previous topics and you will be. (Seriously, read the previous posts — they cover very important concepts you should understand before diving into constructor functions.)
This problem was solved using factory functions, where we created a function that returned this object for us. This function could be called as many times as I wanted, just passing the name
and ownerName
parameters, and the function would take care of the repetitive part and return the object.
function petFactory(name, ownerName) {
return {
name,
ownerName,
identify() {
return ` ${this.name} belongs to ${this.ownerName}`;
}
};
}
const pet1 = petFactory("Katze", "Matheus");
Another way to solve this is by using constructor functions, but unlike factory functions, constructors don’t directly return the created object. That’s where the magical new
and this
come in.
Constructor Functions in the Real World
Before jumping into code, let’s understand the concept. Imagine this scenario: It’s a sunny Sunday in Montreal. Exactly at 3 PM, the Canadian GP starts. Sauber (miraculously) gets an incredible start and is fighting at the front. Engineers and drivers need to stay in constant communication throughout the race to maintain their advantage. It’s a long race, and the engineers notice heavy rain is about to hit and need to tell Gabriel Bortoleto to pit for wet tires. Now imagine if every time they talked, they had to go through a whole dialogue:
- “Hello Gabriel, how’s the race?”
- “Hello Steven, going well, and over there?”
- “All good here. We have an important message. Can we tell you?”
- “Sure, what’s the info?”
- “Well, we need you to box. Heavy rain is coming.”
- “Okay, I’ll box next lap, just passed the pit entrance.”
You can draw two conclusions from this: First, if you’ve followed my content, you know I’m a big F1 fan. Second, this inefficient communication caused trouble for the team and driver. That’s why every team has protocols — a standard to eliminate unnecessary repetition and streamline action. With just one message, Bortoleto knows exactly what to do:
- “Box box, heavy rain next lap.”
- “Understood.”
The concept of factory functions, which we saw earlier, is basically the same: “Give me what’s necessary, and I’ll handle the rest.”
Constructor functions follow the same principle. We want to create a mold, a pattern for creating objects, so we don’t repeat what’s unnecessary — just pass the unique information for that object.
Constructor Functions in Code
Let’s reuse the example from earlier and create a constructor function to register pets in a pet shop.
function Pet(name, ownerName) {
this.name = name;
this.ownerName = ownerName;
this.identify = () => {
return `${this.name} belongs to ${this.ownerName}`;
}
}
const pet1 = new Pet("Katze", "Matheus");
Let’s notice a few things:
- A constructor function borrows the best practices of a class, so it should start with an uppercase letter.
- Unlike a factory function, this function doesn’t return anything.
Focusing on point 2 — if it doesn’t return anything, how does JS know that pet1
should receive an object? That’s where the magic of new
comes in. Under the hood, when the interpreter hits new
, it automatically modifies how the Pet(name, ownerName)
function behaves.
function Pet(name, ownerName) {
// const this = {};
this.name = name;
this.ownerName = ownerName;
this.identify = () => {
return `${this.name} belongs to ${this.ownerName}`;
}
// return this;
}
The two commented lines are code we don’t see, but JS adds behind the scenes. this
is a reserved keyword in JS that references the current execution context. In this case, it refers to the Pet
object. In short, JS creates an object behind the scenes, assigns this
to it, and automatically inherits from Object. (We’ve already covered inheritance in a previous post.)
Comparison
Transforming our previous factory function into a constructor function, we get:
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.updateData = function(newName, newOwner, newSpecies, user) {
if (user.role !== "admin") {
console.warn("Permission denied: only admins can update pet data.");
return;
}
_petName = newName;
_ownerName = newOwner;
_species = newSpecies;
};
this.getData = function() {
return { petName: _petName, ownerName: _ownerName, species: _species };
};
}
// Users
const admin = { name: "Matheus", role: "admin" };
const visitor = { name: "Pedro", role: "user" };
// Creating a pet instance using 'new'
const pet = new Pet("Katze", "Matheus", "Cat");
console.log(pet.identify()); // The Cat Katze belongs to Matheus
pet.updateData("Lion", "Matheus Julidori", "Feline", visitor); // Permission denied
console.log(pet.identify()); // The Cat Katze belongs to Matheus
pet.updateData("Feline", "Matheus Julidori", "Cat", admin); // Allowed
console.log(pet.identify()); // The Cat Feline belongs to Matheus Julidori
Again, all the problems we discussed about factory functions are solved here. And similarly, we can use this in a frontend application:
</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 PetCard(pet) {
const { petName, ownerName, species } = pet.getData();
this.card = document.createElement("div");
this.card.className = "card-pet";
this.title = document.createElement("h3");
this.title.textContent = petName;
this.card.appendChild(this.title);
this.speciesEl = document.createElement("p");
this.speciesEl.textContent = `Species: ${species}`;
this.card.appendChild(this.speciesEl);
this.ownerEl = document.createElement("p");
this.ownerEl.textContent = `Owner: ${ownerName}`;
this.card.appendChild(this.ownerEl);
}
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);
const newPetCard = new PetCard(newPet);
document.getElementById("area-pets").appendChild(newPetCard.card);
form.reset();
});
Enter fullscreen mode
Exit fullscreen mode
So, we solved the same problem using two different approaches. But notice that, in this frontend case, some things became slightly more complex (perhaps unnecessarily) when using constructor functions instead of factory functions. And that leads us to the million-dollar question: “Which one should I use, and what’s the real difference between them?”That’s exactly what we’ll cover in the next post, which will be entirely dedicated to answering that question.
Final Thoughts
Constructor functions are an essential piece of JavaScript's object construction toolbox. While factory functions offer more flexibility and control, constructor functions provide a more classical, OOP-inspired syntax that many developers coming from other languages will recognize.They both solve the same core issues: code repetition, maintenance headaches, scalability, and structure. Whether you prefer new and this, or simple return statements in closures, understanding both patterns will help you write more robust, readable, and maintainable JavaScript.
Factory Function
Constructor Function
return {}
uses this
No new keyword
Requires new
More flexible
Familiar to OOP devs
Can be used as closures
Can use prototypes
💬 Have any thoughts, questions, or cool examples you've come across?Drop a comment — I’d love to learn how you’re using this in your projects!📬 If you're enjoying the series, consider bookmarking it or sharing with a fellow JS dev. Let’s grow together!👉 Follow me @matheusjulidori for the next episodes of Do You Know How It Works?Next up: We're finally answering the big question: Factory or Constructor — which one should you use, and when?
Don’t miss it.