Skip to content

Constructor Pattern: The Core Mechanism of JavaScript Object Creation

When building applications, we often need to create multiple objects with the same structure but containing different data—such as user lists in a system or products in a shopping cart. Manually writing object literals each time is not only inefficient but also difficult to maintain. The constructor pattern was created to solve this problem, providing a standardized way to batch create object instances.

Basic Concepts of Constructors

Constructors are special functions in JavaScript used to create objects of a specific type. By convention, constructor names usually start with a capital letter to distinguish them from regular functions.

Basic Constructor

javascript
// Basic constructor
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;

  // Define method in constructor (not recommended)
  this.sayName = function () {
    console.log(this.name);
  };
}

// Create instances using the new operator
const person1 = new Person("John", 25, "Developer");
const person2 = new Person("Sarah", 30, "Designer");

console.log(person1.name); // John
console.log(person2.age); // 30

person1.sayName(); // John
person2.sayName(); // Sarah

How the new Operator Works

When calling a constructor with the new operator, the JavaScript engine performs the following four steps:

  1. Create new object: Create a new empty JavaScript object
  2. Set prototype chain: Point the new object's [[Prototype]] to the constructor's prototype
  3. Bind this: Point this in the constructor to the newly created object
  4. Return object: If the constructor doesn't explicitly return another object, return the newly created object
javascript
function Person(name) {
  // Step 3: this points to the newly created object
  this.name = name;
  this.sayName = function () {
    console.log(this.name);
  };

  // Demonstrate different return scenarios
  // If no explicit return, return this (newly created object)
  // If returning basic type, still return this
  // If returning object, return that object (not this)
}

// Step 1: Create new object {}
// Step 2: Set prototype chain: {}.__proto__ = Person.prototype
// Step 3: Bind this and execute constructor
// Step 4: Return new object
const person = new Person("John");

Simulating new Operator Implementation

To better understand the new operator, we can implement a similar function ourselves:

javascript
function myNew(constructor, ...args) {
  // 1. Create new object
  const obj = {};

  // 2. Set prototype chain
  obj.__proto__ = constructor.prototype;

  // 3. Bind this and execute constructor
  const result = constructor.apply(obj, args);

  // 4. Return result (if constructor returns object, return that object, otherwise return newly created object)
  return typeof result === "object" && result !== null ? result : obj;
}

// Use our myNew function
function Person(name) {
  this.name = name;
  this.sayName = function () {
    console.log(this.name);
  };
}

const person = myNew(Person, "John");
console.log(person.name); // John
person.sayName(); // John

console.log(person.__proto__ === Person.prototype); // true

Differences Between Constructors and Regular Functions

Differences in Calling Methods

javascript
function Person(name) {
  this.name = name;
  console.log("this:", this);
}

// 1. Called as constructor
const person = new Person("John");
// this: Person { name: 'John' }

// 2. Called as regular function
Person("John");
// this: Window (undefined in strict mode)

Return Value Handling

javascript
function TestConstructor() {
  this.prop = "created";

  // Case 1: No return statement
  // const obj1 = new TestConstructor();
  // obj1 is { prop: 'created' }

  // Case 2: Return basic type
  // return 42;
  // const obj2 = new TestConstructor();
  // obj2 is still { prop: 'created' }

  // Case 3: Return object
  // return { custom: 'object' };
  // const obj3 = new TestConstructor();
  // obj3 is { custom: 'object' }
}

const obj = new TestConstructor();
console.log(obj.prop); // 'created'

Relationship Between Prototypes and Constructors

The prototype Property

Every function has a prototype property. This property is an object containing properties and methods that can be shared by instances created by that function.

javascript
function Car(brand, model) {
  this.brand = brand;
  this.model = model;
}

// Define methods on prototype, shared by all instances
Car.prototype.start = function () {
  console.log(`${this.brand} ${this.model} is starting`);
};

Car.prototype.stop = function () {
  console.log(`${this.brand} ${this.model} is stopping`);
};

const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Honda", "Civic");

car1.start(); // Toyota Camry is starting
car2.start(); // Honda Civic is starting

// Verify methods are indeed shared
console.log(car1.start === car2.start); // true

// Verify prototype chain relationship
console.log(car1.__proto__ === Car.prototype); // true
console.log(car2.__proto__ === Car.prototype); // true

The constructor Property

By default, prototype objects have a constructor property that points back to the constructor:

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

console.log(Person.prototype.constructor === Person); // true

const person = new Person("John");
console.log(person.constructor === Person); // true
console.log(person.__proto__.constructor === Person); // true

Best Practices: Define Methods on Prototypes

Method Definition Location Selection

javascript
// ❌ Not recommended: Define methods in constructor
function BadExample(name) {
  this.name = name;

  // Each time an instance is created, a new function object is created
  this.sayName = function () {
    console.log(this.name);
  };
}

const person1 = new BadExample("John");
const person2 = new BadExample("Sarah");

console.log(person1.sayName === person2.sayName); // false
// Each instance has its own sayName function, wasting memory
javascript
// ✅ Recommended: Define methods on prototype
function GoodExample(name) {
  this.name = name;
}

GoodExample.prototype.sayName = function () {
  console.log(this.name);
};

const person1 = new GoodExample("John");
const person2 = new GoodExample("Sarah");

console.log(person1.sayName === person2.sayName); // true
// All instances share the same sayName function

Constructor Template

This is a commonly used constructor template:

javascript
function Animal(species, habitat) {
  // Properties unique to each instance
  this.species = species;
  this.habitat = habitat;
  this.age = 0;

  // If needed, you can define private methods here
  // But it's better to use module pattern
}

// Shared methods defined on prototype
Animal.prototype.eat = function () {
  console.log(`${this.species} is eating`);
  this.age++;
};

Animal.prototype.sleep = function () {
  console.log(`${this.species} is sleeping`);
};

Animal.prototype.getAge = function () {
  return this.age;
};

// Static properties and methods (belong to constructor itself)
Animal.createHuman = function () {
  return new Animal("Human", "Urban");
};

Animal.defaultDiet = "omnivore";

// Use constructor
const lion = new Animal("Lion", "Savanna");
lion.eat(); // Lion is eating
lion.sleep(); // Lion is sleeping

// Use static methods
const human = Animal.createHuman();
console.log(human.species); // Human
console.log(Animal.defaultDiet); // omnivore

Constructor Inheritance

Implementing Inheritance Using Prototype Chain

javascript
function Animal(species) {
  this.species = species;
}

Animal.prototype.move = function () {
  console.log(`${this.species} is moving`);
};

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

// Inherit prototype (inherit methods)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix constructor reference

// Add child-specific methods
Dog.prototype.bark = function () {
  console.log(`${this.name} barks: Woof!`);
};

// Override parent method
Dog.prototype.move = function () {
  console.log(`${this.name} the ${this.breed} is running`);
};

const dog = new Dog("Buddy", "Golden Retriever");

// Call child method
dog.bark(); // Buddy barks: Woof!

// Call overridden method
dog.move(); // Buddy the Golden Retriever is running

// Call parent method (through prototype chain)
Animal.prototype.move.call(dog); // Dog is moving

Practical Inheritance Helper Function

javascript
function inherit(child, parent) {
  // Inherit prototype
  child.prototype = Object.create(parent.prototype);
  // Fix constructor
  child.prototype.constructor = child;
  // Add reference to parent class
  child.super = parent;
}

function Vehicle(wheels) {
  this.wheels = wheels;
}

Vehicle.prototype.start = function () {
  console.log("Vehicle started");
};

function Car(brand, model) {
  Vehicle.super.call(this, 4); // Call parent constructor
  this.brand = brand;
  this.model = model;
}

inherit(Car, Vehicle);

Car.prototype.start = function () {
  console.log(`${this.brand} ${this.model} engine started`);
};

const car = new Car("Toyota", "Camry");
car.start(); // Toyota Camry engine started
console.log(car.wheels); // 4

Modular Constructor Pattern

IIFE Pattern to Protect Constructors

javascript
const PersonModule = (function () {
  "use strict";

  // Private variables and functions
  let personCount = 0;

  function validateName(name) {
    return typeof name === "string" && name.length > 0;
  }

  // Constructor
  function Person(name, age) {
    if (!validateName(name)) {
      throw new Error("Invalid name");
    }

    if (age < 0) {
      throw new Error("Age cannot be negative");
    }

    this.name = name;
    this.age = age;
    this.id = ++personCount;
  }

  // Prototype methods
  Person.prototype.sayName = function () {
    console.log(`My name is ${this.name}`);
  };

  Person.prototype.sayAge = function () {
    console.log(`I am ${this.age} years old`);
  };

  // Static methods
  Person.getPersonCount = function () {
    return personCount;
  };

  Person.createAdult = function (name) {
    return new Person(name, 18);
  };

  return Person;
})();

// Use modular constructor
const person1 = new PersonModule("John", 25);
const person2 = new PersonModule("Sarah", 30);

person1.sayName(); // My name is John
person2.sayAge(); // I am 30 years old

console.log(PersonModule.getPersonCount()); // 2

const adult = PersonModule.createAdult("Mike");
console.log(adult.age); // 18

Constructor Pattern Traps and Precautions

1. Forgetting to Use the new Operator

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

// ❌ Forgetting to use new
const person = Person("John"); // this points to global object (non-strict mode) or undefined (strict mode)
console.log(window.name); // 'John' (pollutes global variable)

// ✅ Correctly using new
const person2 = new Person("John");
console.log(person2.name); // 'John'

Solution: Self-calling Constructor

javascript
function Person(name) {
  // Ensure always called with new
  if (!(this instanceof Person)) {
    return new Person(name);
  }

  this.name = name;
}

// Now both ways work
const person1 = new Person("John");
const person2 = Person("Sarah");

console.log(person1.name); // John
console.log(person2.name); // Sarah

2. Prototype Accidentally Overridden

javascript
function Car(brand) {
  this.brand = brand;
}

Car.prototype.drive = function () {
  console.log(`${this.brand} is driving`);
};

const car = new Car("Toyota");

// ❌ Overwrite entire prototype
Car.prototype = {
  fly: function () {
    console.log(`${this.brand} is flying`);
  },
};

console.log(car.drive); // undefined, because car's prototype didn't change
car.fly(); // TypeError: car.fly is not a function

3. Reference Type Properties on Prototype

javascript
function Family() {
  // Instance properties
  this.familyName = "Smith";
}

// ❌ Define reference type on prototype
Family.prototype.members = []; // All instances share the same array

const family1 = new Family();
const family2 = new Family();

family1.members.push("John");
console.log(family2.members); // ['John'] - Unexpected sharing!

// ✅ Correct approach
Family.prototype.getMembers = function () {
  // Each instance has its own members array
  if (!this._members) {
    this._members = [];
  }
  return this._members;
};

family1.getMembers().push("John");
console.log(family2.getMembers()); // [] - Independent array

Modern JavaScript Alternatives

Class Syntax

Modern JavaScript provides friendlier Class syntax, but underneath it's still based on constructors and prototypes:

javascript
// Modern Class syntax
class Person {
  // Constructor
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // Instance methods (equivalent to prototype methods)
  sayName() {
    console.log(`My name is ${this.name}`);
  }

  // Static methods
  static createAdult(name) {
    return new Person(name, 18);
  }

  // Getter
  get info() {
    return `${this.name} (${this.age} years old)`;
  }
}

// Use Class
const person = new Person("John", 25);
person.sayName(); // My name is John
console.log(person.info); // John (25 years old)

const adult = Person.createAdult("Sarah");
console.log(adult.age); // 18

Factory Function Pattern

Sometimes, factory functions are more flexible than constructors:

javascript
function createPerson(name, age) {
  return {
    name,
    age,
    sayName() {
      console.log(`My name is ${this.name}`);
    },
    sayAge() {
      console.log(`I am ${this.age} years old`);
    },
  };
}

const person = createPerson("John", 25);
person.sayName(); // My name is John

// Advantages: No need to use new, can return different object structures
// Disadvantages: No explicit type checking, each instance has its own method copy

Summary

The constructor pattern is the fundamental mechanism for creating objects in JavaScript, and understanding it is crucial for mastering JavaScript object-oriented programming:

  • Constructors are special functions used to create objects of specific types
  • The new operator is responsible for creating objects, setting prototypes, binding this, and returning objects
  • Methods should be defined on prototypes to achieve memory sharing and performance optimization
  • Inheritance requires combining prototype chains and constructor calls to implement complete property and method inheritance
  • Watch out for common pitfalls like forgetting new, prototype overwriting, and reference type sharing

Although modern JavaScript provides Class syntax and other friendlier APIs, the underlying mechanism is still based on constructors and prototypes. Understanding how constructors work helps us better understand JavaScript's object-oriented features and write more efficient, robust code.