Skip to main content
โšก Calmops

Constructor Functions and the 'new' Keyword in JavaScript

Constructor Functions and the ’new’ Keyword in JavaScript

Introduction

Constructor functions are a fundamental pattern in JavaScript for creating multiple objects with the same structure and behavior. Before ES6 classes were introduced, constructor functions were the primary way to create object instances. Understanding constructor functions is essential for working with JavaScript’s prototype-based inheritance system.

In this article, you’ll learn how to create and use constructor functions, understand what the new keyword does, and explore best practices for this important pattern.

What is a Constructor Function?

A constructor function is a regular function that is designed to be called with the new keyword to create new objects. By convention, constructor function names start with a capital letter.

// Constructor function
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Creating instances with the 'new' keyword
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

console.log(person1); // Person { name: 'Alice', age: 30 }
console.log(person2); // Person { name: 'Bob', age: 25 }

Understanding the ’new’ Keyword

When you call a function with the new keyword, JavaScript performs four steps:

  1. Creates a new empty object
  2. Sets the object’s prototype to the constructor’s prototype
  3. Calls the constructor function with this bound to the new object
  4. Returns the new object (unless the constructor explicitly returns an object)
// What happens when you use 'new'
function Car(make, model) {
  // Step 1: new object created (implicitly)
  // Step 2: this = {} with Car.prototype as its prototype
  
  this.make = make;
  this.model = model;
  
  // Step 4: return this (implicitly)
}

const myCar = new Car('Toyota', 'Camry');
console.log(myCar); // Car { make: 'Toyota', model: 'Camry' }
console.log(myCar instanceof Car); // true

Visualizing the ’new’ Keyword

// Manual simulation of what 'new' does
function Person(name) {
  this.name = name;
}

// Without 'new' (wrong way)
const person1 = Person('Alice');
console.log(person1); // undefined
console.log(this.name); // 'Alice' (added to global object - bad!)

// With 'new' (correct way)
const person2 = new Person('Bob');
console.log(person2); // Person { name: 'Bob' }
console.log(person2.name); // 'Bob'

Constructor Functions with Methods

You can add methods to constructor functions using the prototype property.

// Constructor function
function Animal(name, species) {
  this.name = name;
  this.species = species;
}

// Adding methods to the prototype
Animal.prototype.speak = function() {
  return `${this.name} makes a sound`;
};

Animal.prototype.getInfo = function() {
  return `${this.name} is a ${this.species}`;
};

// Creating instances
const dog = new Animal('Rex', 'Dog');
const cat = new Animal('Whiskers', 'Cat');

console.log(dog.speak()); // Rex makes a sound
console.log(cat.getInfo()); // Whiskers is a Cat

Constructor Function Patterns

Pattern 1: Basic Constructor

function Book(title, author, year) {
  this.title = title;
  this.author = author;
  this.year = year;
}

Book.prototype.getAge = function() {
  return new Date().getFullYear() - this.year;
};

const book1 = new Book('1984', 'George Orwell', 1949);
console.log(book1.getAge()); // 76 (as of 2025)

Pattern 2: Constructor with Private Properties

function BankAccount(initialBalance) {
  let balance = initialBalance; // Private variable
  
  this.deposit = function(amount) {
    balance += amount;
    return balance;
  };
  
  this.withdraw = function(amount) {
    if (amount <= balance) {
      balance -= amount;
      return balance;
    }
    return 'Insufficient funds';
  };
  
  this.getBalance = function() {
    return balance;
  };
}

const account = new BankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined (private)

Pattern 3: Constructor with Default Values

function User(name, email, role = 'user') {
  this.name = name;
  this.email = email;
  this.role = role;
  this.createdAt = new Date();
}

const user1 = new User('Alice', '[email protected]');
const user2 = new User('Bob', '[email protected]', 'admin');

console.log(user1); // User { name: 'Alice', email: '[email protected]', role: 'user', ... }
console.log(user2); // User { name: 'Bob', email: '[email protected]', role: 'admin', ... }

Pattern 4: Constructor with Validation

function Email(address) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  
  if (!emailRegex.test(address)) {
    throw new Error('Invalid email address');
  }
  
  this.address = address;
}

try {
  const email1 = new Email('[email protected]');
  console.log(email1); // Email { address: '[email protected]' }
  
  const email2 = new Email('invalid-email');
} catch (error) {
  console.log(error.message); // Invalid email address
}

Constructor Functions vs Regular Functions

Key Differences

// Constructor function (called with 'new')
function Person(name) {
  this.name = name;
}

const person = new Person('Alice');
console.log(person); // Person { name: 'Alice' }

// Regular function (called without 'new')
function greet(name) {
  return `Hello, ${name}`;
}

const greeting = greet('Bob');
console.log(greeting); // Hello, Bob

// What happens if you call a constructor without 'new'?
const person2 = Person('Charlie');
console.log(person2); // undefined
console.log(this.name); // 'Charlie' (added to global object - bad!)

Preventing Accidental Calls Without ’new'

function SafeConstructor(name) {
  // Check if called with 'new'
  if (!(this instanceof SafeConstructor)) {
    return new SafeConstructor(name);
  }
  
  this.name = name;
}

// Works with 'new'
const obj1 = new SafeConstructor('Alice');
console.log(obj1); // SafeConstructor { name: 'Alice' }

// Also works without 'new' (automatically uses 'new')
const obj2 = SafeConstructor('Bob');
console.log(obj2); // SafeConstructor { name: 'Bob' }

Constructor Functions with Inheritance

Basic Inheritance Pattern

// Parent constructor
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  return `${this.name} makes a sound`;
};

// Child constructor
function Dog(name, breed) {
  Animal.call(this, name); // Call parent constructor
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add dog-specific methods
Dog.prototype.bark = function() {
  return `${this.name} barks`;
};

const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // Rex makes a sound
console.log(dog.bark()); // Rex barks
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

Inheritance with Super-like Behavior

function Vehicle(make, model) {
  this.make = make;
  this.model = model;
}

Vehicle.prototype.getInfo = function() {
  return `${this.make} ${this.model}`;
};

function Car(make, model, doors) {
  Vehicle.call(this, make, model);
  this.doors = doors;
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

Car.prototype.getInfo = function() {
  const parentInfo = Vehicle.prototype.getInfo.call(this);
  return `${parentInfo} with ${this.doors} doors`;
};

const car = new Car('Toyota', 'Camry', 4);
console.log(car.getInfo()); // Toyota Camry with 4 doors

Practical Real-World Examples

Example 1: User Management System

function User(username, email, password) {
  this.username = username;
  this.email = email;
  this.password = password;
  this.createdAt = new Date();
  this.isActive = true;
}

User.prototype.deactivate = function() {
  this.isActive = false;
};

User.prototype.updateEmail = function(newEmail) {
  this.email = newEmail;
};

User.prototype.getProfile = function() {
  return {
    username: this.username,
    email: this.email,
    createdAt: this.createdAt,
    isActive: this.isActive
  };
};

const user = new User('john_doe', '[email protected]', 'secret123');
console.log(user.getProfile());
user.updateEmail('[email protected]');
console.log(user.getProfile());

Example 2: Shopping Cart

function ShoppingCart() {
  this.items = [];
  this.total = 0;
}

ShoppingCart.prototype.addItem = function(name, price, quantity = 1) {
  this.items.push({ name, price, quantity });
  this.total += price * quantity;
};

ShoppingCart.prototype.removeItem = function(name) {
  const item = this.items.find(i => i.name === name);
  if (item) {
    this.total -= item.price * item.quantity;
    this.items = this.items.filter(i => i.name !== name);
  }
};

ShoppingCart.prototype.getTotal = function() {
  return this.total.toFixed(2);
};

ShoppingCart.prototype.getItems = function() {
  return this.items;
};

const cart = new ShoppingCart();
cart.addItem('Laptop', 999.99, 1);
cart.addItem('Mouse', 29.99, 2);
console.log(cart.getItems());
console.log('Total:', cart.getTotal());

Example 3: Task Manager

function Task(title, description, priority = 'medium') {
  this.id = Date.now();
  this.title = title;
  this.description = description;
  this.priority = priority;
  this.completed = false;
  this.createdAt = new Date();
}

Task.prototype.complete = function() {
  this.completed = true;
};

Task.prototype.updatePriority = function(newPriority) {
  this.priority = newPriority;
};

Task.prototype.getStatus = function() {
  return this.completed ? 'Completed' : 'Pending';
};

function TaskList() {
  this.tasks = [];
}

TaskList.prototype.addTask = function(title, description, priority) {
  const task = new Task(title, description, priority);
  this.tasks.push(task);
  return task;
};

TaskList.prototype.completeTask = function(taskId) {
  const task = this.tasks.find(t => t.id === taskId);
  if (task) {
    task.complete();
  }
};

TaskList.prototype.getTasks = function(filter = 'all') {
  if (filter === 'completed') {
    return this.tasks.filter(t => t.completed);
  }
  if (filter === 'pending') {
    return this.tasks.filter(t => !t.completed);
  }
  return this.tasks;
};

const taskList = new TaskList();
taskList.addTask('Learn JavaScript', 'Master constructor functions', 'high');
taskList.addTask('Build a project', 'Create a real-world application', 'medium');
console.log(taskList.getTasks());

Common Mistakes to Avoid

Mistake 1: Forgetting the ’new’ Keyword

// โŒ Wrong - Forgot 'new'
function Person(name) {
  this.name = name;
}

const person = Person('Alice');
console.log(person); // undefined
console.log(this.name); // 'Alice' (pollutes global scope)

// โœ… Correct
const person = new Person('Alice');
console.log(person); // Person { name: 'Alice' }

Mistake 2: Not Setting Up Prototype Correctly

// โŒ Wrong - Methods not shared
function Animal(name) {
  this.name = name;
  this.speak = function() { // Created for each instance
    return `${this.name} speaks`;
  };
}

// โœ… Correct - Methods shared via prototype
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  return `${this.name} speaks`;
};

Mistake 3: Incorrect Inheritance Setup

// โŒ Wrong - Breaks prototype chain
function Dog(name) {
  Animal.call(this, name);
}
Dog.prototype = Animal.prototype; // Wrong!

// โœ… Correct - Maintains prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Mistake 4: Modifying Constructor Prototype After Creating Instances

// โŒ Problematic - Changes affect all instances
function Person(name) {
  this.name = name;
}

const person1 = new Person('Alice');

Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

const person2 = new Person('Bob');

// Both can use greet, but it's confusing
console.log(person1.greet()); // Works
console.log(person2.greet()); // Works

// โœ… Better - Define all methods before creating instances
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

const person1 = new Person('Alice');
const person2 = new Person('Bob');

Constructor Functions vs ES6 Classes

Modern JavaScript provides ES6 classes, which are syntactic sugar over constructor functions.

// Constructor function (traditional)
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

// ES6 class (modern)
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// Both work the same way
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

Summary

Constructor functions are a fundamental pattern in JavaScript:

  • Constructor functions are regular functions called with the new keyword
  • The new keyword creates a new object and sets up the prototype chain
  • Methods should be defined on the prototype for memory efficiency
  • Constructor functions can be used for inheritance with Object.create()
  • Always use the new keyword when calling constructor functions
  • Modern ES6 classes provide a cleaner syntax for the same pattern
  • Prevent accidental calls without new using instanceof checks

Next Steps

Continue your learning journey:

Comments