JavaScript Prototypes & Inheritance

JavaScript uses a prototype-based inheritance model that's different from class-based inheritance in languages like Java or C++. Understanding prototypes is essential for mastering JavaScript's object-oriented programming capabilities.

What are Prototypes?

In JavaScript, every object has a hidden property called Prototype (accessible via __proto__ or Object.getPrototypeOf()), which points to another object called its "prototype". When you try to access a property that doesn't exist on an object, JavaScript automatically looks for it in the object's prototype, then in the prototype's prototype, and so on, forming what's called the "prototype chain".

// Create an object
const animal = {
  eats: true,
  walk() {
    console.log('Animal walking');
  }
};

// Create another object with animal as its prototype
const rabbit = Object.create(animal);
rabbit.jumps = true;

// Access properties
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats);  // true (inherited from animal)

// Call methods
rabbit.walk();  // "Animal walking" (method from animal)

Note: The __proto__ property is deprecated for direct use in code. Use Object.getPrototypeOf() and Object.setPrototypeOf() instead.

Constructor Functions and Prototypes

Before ES6 classes, constructor functions were the primary way to create "class-like" functionality in JavaScript. Each constructor function has a prototype property, which becomes the Prototype of objects created with that constructor.

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

// Adding a method to the prototype
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
};

// Create instances
const dog = new Animal('Rex');
const cat = new Animal('Whiskers');

dog.speak(); // "Rex makes a noise."
cat.speak(); // "Whiskers makes a noise."

// All instances share the same method
console.log(dog.speak === cat.speak); // true

The Prototype Chain

When you create an object with a constructor, the object's Prototype points to the constructor's prototype property. This creates a chain of inheritance.

// Check the prototype chain
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of the chain)
Prototype Chain Diagram

Diagram: The prototype chain from a dog instance to Object.prototype

Image generated using Placeholder.com. Free to use for any purpose.

Inheritance with Prototypes

You can create inheritance hierarchies by linking prototypes together.

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

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

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

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

// Add a method to Dog.prototype
Dog.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};

// Create an instance
const rex = new Dog('Rex', 'German Shepherd');
rex.eat();  // "Rex is eating." (inherited from Animal)
rex.bark(); // "Rex barks!" (from Dog)

ES6 Classes and Inheritance

ES6 introduced class syntax, which provides a cleaner way to work with prototypes and inheritance. Under the hood, it still uses the prototype-based inheritance model.

// Parent class
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

// Child class extending Animal
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} barks!`);
  }
}

// Create an instance
const rex = new Dog('Rex', 'German Shepherd');
rex.eat();  // "Rex is eating." (inherited from Animal)
rex.bark(); // "Rex barks!" (from Dog)

Tip: ES6 classes are just syntactic sugar over JavaScript's prototype-based inheritance. Understanding prototypes helps you understand what's happening under the hood with classes.

Static Methods and Properties

Static methods and properties belong to the constructor/class itself, not to instances.

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

// Instance method
Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

// Static method
Animal.compare = function(a, b) {
  return a.name === b.name;
};

// Using ES6 classes
class Pet {
  constructor(name) {
    this.name = name;
  }
  
  // Instance method
  feed() {
    console.log(`Feeding ${this.name}`);
  }
  
  // Static method
  static create(name) {
    return new Pet(name);
  }
}

const fluffy = Pet.create('Fluffy');
fluffy.feed(); // "Feeding Fluffy"

Object.create() vs Constructor Functions vs Classes

JavaScript offers multiple ways to create objects and implement inheritance. Here's a comparison:

// 1. Using Object.create()
const animalProto = {
  eat() {
    console.log(`${this.name} is eating.`);
  }
};

const dog = Object.create(animalProto);
dog.name = 'Rex';
dog.bark = function() {
  console.log(`${this.name} barks!`);
};

// 2. Using Constructor Functions
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};

const rex = new Dog('Rex', 'German Shepherd');

// 3. Using ES6 Classes
class AnimalClass {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class DogClass extends AnimalClass {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} barks!`);
  }
}

const buddy = new DogClass('Buddy', 'Golden Retriever');

Common Prototype Patterns

Prototype Delegation

This pattern uses Object.create() to delegate behavior to a prototype object.

// Task prototype with shared methods
const taskPrototype = {
  complete() {
    this.completed = true;
    console.log(`Completed: ${this.description}`);
  },
  
  toString() {
    return `${this.description} - ${this.completed ? 'Completed' : 'Pending'}`;
  }
};

// Factory function to create tasks
function createTask(description) {
  return Object.create(taskPrototype, {
    description: { value: description, writable: true },
    completed: { value: false, writable: true }
  });
}

const task1 = createTask('Learn prototypes');
const task2 = createTask('Master JavaScript');

task1.complete(); // "Completed: Learn prototypes"
console.log(task1.toString()); // "Learn prototypes - Completed"
console.log(task2.toString()); // "Master JavaScript - Pending"

Mixins

Mixins allow you to compose objects by copying properties from multiple sources.

// Mixin objects with reusable functionality
const swimmer = {
  swim() {
    console.log(`${this.name} is swimming.`);
  }
};

const flyer = {
  fly() {
    console.log(`${this.name} is flying.`);
  }
};

// Base class
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

// Create a Duck class that uses both mixins
class Duck extends Animal {
  constructor(name) {
    super(name);
  }
}

// Apply mixins
Object.assign(Duck.prototype, swimmer, flyer);

const donald = new Duck('Donald');
donald.eat();  // "Donald is eating."
donald.swim(); // "Donald is swimming."
donald.fly();  // "Donald is flying."

Prototype Pitfalls and Best Practices

Modifying Built-in Prototypes

Modifying built-in prototypes like Array.prototype or Object.prototype is generally considered a bad practice.

Warning: Extending built-in prototypes can lead to naming conflicts, compatibility issues, and unexpected behavior in third-party libraries.

// Bad practice
Array.prototype.first = function() {
  return this[0];
};

// Better approach: Create a utility function
function getFirst(array) {
  return array[0];
}

// Or use a custom class that extends Array
class MyArray extends Array {
  first() {
    return this[0];
  }
}

Property Shadowing

When an object has a property with the same name as a property in its prototype chain, the object's own property "shadows" the prototype property.

const animal = {
  speak() {
    console.log('Animal sound');
  }
};

const dog = Object.create(animal);

// This shadows the speak method from the prototype
dog.speak = function() {
  console.log('Woof!');
};

dog.speak(); // "Woof!"

// To call the prototype method
animal.speak.call(dog); // "Animal sound"

Interactive Demo

Try out this interactive example to see prototypes and inheritance in action:

Create a Vehicle

Name:
Wheels:

Create a Car (inherits from Vehicle)

Name:
Brand:

Object Information:

Create an object to see its properties and prototype chain.

Next Steps

Now that you understand JavaScript prototypes and inheritance, you can explore related topics:

  • Object-Oriented Design Patterns in JavaScript
  • ES6+ Class Features (getters, setters, private fields)
  • Composition vs. Inheritance
  • Functional Programming in JavaScript
  • TypeScript and its class-based approach