The Constructor/Prototype Pattern is the most widely adopted pattern for creating custom types in traditional JavaScript (ES5 and earlier). It is a hybrid approach that combines the best aspects of the Constructor Pattern and the Prototype Pattern to solve a specific problem: memory efficiency versus instance-specific data.

In this model, the constructor defines instance properties, while the prototype defines shared methods and properties.

1. The Core Problem: The Memory Leak of Constructors

If you define methods inside a constructor using this, every new object instance gets its own copy of those functions. If you create 1,000 instances, you create 1,000 identical function objects in memory.

JavaScript
// BAD: The "Constructor-only" approach
function User(name) {
  this.name = name;
  this.sayHi = function() { // Redundant copy created for every instance
    console.log(this.name);
  };
}

2. The Hybrid Solution: Constructor/Prototype Pattern

This pattern splits the object definition into two distinct layers:

  • The Constructor: Used to initialize unique data for each instance (e.g., names, IDs, coordinates).
  • The Prototype: Used to store shared logic (methods) and constant values that all instances should use.

Implementation Example

JavaScript
// 1. Define instance-specific properties in the Constructor
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.isStarted = false;
}

// 2. Define shared methods on the Prototype
Car.prototype.start = function() {
  this.isStarted = true;
  console.log(`${this.make} ${this.model} has started.`);
};

Car.prototype.drive = function() {
  if (this.isStarted) {
    console.log("Vroom!");
  } else {
    console.log("Start the car first.");
  }
};

const myCar = new Car("Tesla", "Model 3", 2024);
const yourCar = new Car("Ford", "Mustang", 1967);

myCar.start(); // Tesla Model 3 has started.

In this example, myCar and yourCar have different values for make and model, but they reference the exact same start and drive functions in memory.

3. Implementing the Pattern

Step 1: The Constructor (The Data)

The constructor is a regular function named with a capital letter (by convention). We use the this keyword to assign properties.

JavaScript
function Car(make, model, year) {
  // Unique data for every instance
  this.make = make;
  this.model = model;
  this.year = year;
  this.fuel = 100;
}

Step 2: The Prototype (The Behavior)

We attach methods to the function’s prototype property. These are defined only once in memory.

JavaScript
Car.prototype.drive = function(distance) {
  this.fuel -= distance * 0.5;
  console.log(`${this.make} ${this.model} drove ${distance} miles. Fuel left: ${this.fuel}%`);
};

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

Step 3: Instantiation

When we use the new keyword, JavaScript creates a new object, sets its internal [[Prototype]] to Car.prototype, and executes the constructor.

JavaScript
const myCar = new Car("Tesla", "Model 3", 2022);
const yourCar = new Car("Ford", "Mustang", 1969);

myCar.drive(10);  // "Tesla Model 3 drove 10 miles. Fuel left: 95%"
console.log(myCar.drive === yourCar.drive); // true (Shared reference!)

4. Encapsulation and “Static” Properties

In this pattern, you can also simulate Static Properties (properties belonging to the “class” rather than an instance) by attaching them directly to the constructor function.

JavaScript
Car.totalCarsCreated = 0; // Static property

function Car(make) {
  this.make = make;
  Car.totalCarsCreated++;
}

5. Inheritance: Extending the Pattern

To create a “sub-type” (like an ElectricCar that inherits from Car), we must perform two steps: Constructor Stealing and Prototype Linking.

Step A: Constructor Stealing (Inheriting Properties)

We call the parent constructor inside the child constructor using .call() or .apply() to ensure the child gets the parent’s properties.

JavaScript
function ElectricCar(make, model, year, batteryRange) {
  // "Borrow" properties from Car
  Car.call(this, make, model, year);
  this.batteryRange = batteryRange;
}

Step B: Prototype Linking (Inheriting Methods)

We must set the child’s prototype to be an instance of the parent’s prototype.

JavaScript
// Link prototypes
ElectricCar.prototype = Object.create(Car.prototype);

// Fix the constructor pointer (otherwise it points to Car)
ElectricCar.prototype.constructor = ElectricCar;

ElectricCar.prototype.charge = function() {
  this.fuel = 100;
  console.log("Battery fully charged.");
};

6. Dynamic Prototype Variations

In some cases, developers use the Dynamic Prototype Pattern, which encapsulates the prototype definition inside the constructor itself. This ensures that the methods are only defined once, but keeps all “class” logic in one physical block of code.

JavaScript
function Person(name, age) {
  this.name = name;
  this.age = age;

  // Initialize prototype methods only if they don't exist
  if (typeof this.sayName !== "function") {
    Person.prototype.sayName = function() {
      console.log(this.name);
    };
  }
}

7. Evolution: ES6 Classes

While the Constructor/Prototype pattern is the underlying logic of JavaScript, modern development uses the class syntax. It is important to realize that class is mostly syntactic sugar over this exact pattern.

JavaScript
class Car {
  constructor(make, model) {
    this.make = make; // Becomes instance property
  }
  start() { // Becomes Car.prototype.start
    console.log("Starting...");
  }
}

In the ES6 version, the engine automatically handles the prototype linking and constructor assignment that you would otherwise do manually in the older pattern.

Categorized in:

Javascript,