JavaScript Object-Oriented Programming: Prototypical Inheritance

JavaScript Object-Oriented Programming: Prototypical Inheritance

JavaScript’s object system is unique compared to many classical languages because it is based on prototypical inheritance. Unlike classical inheritance where classes inherit from other classes, JavaScript objects inherit directly from other objects through a prototype chain. This model allows objects to share properties and methods by linking to a prototype object, which can then link to another prototype, and so forth. Understanding prototypical inheritance is essential to mastering JavaScript’s flexible and powerful object system.

In this article, we will explore how to use prototypical inheritance in JavaScript by focusing on practical “how-to” examples. You will learn to create objects that inherit properties and methods from other objects, how to override inherited behavior, and how prototype chains work behind the scenes. The examples are designed to be fun and engaging, helping you see inheritance in action with relatable and creative code.

Creating an Object with a Prototype Using Object.create()

One of the simplest ways to set up prototypical inheritance in JavaScript is by using the built-in Object.create() method. This method creates a new object, setting the prototype of that object to another object you specify.

Let’s start by creating a basic object named Car. This Car object will have some properties such as make and year. Then, we will create a new object named Tesla that inherits from Car and adds its own unique property.

const Car = {

  make: "Generic",
  year: 2000,
  startEngine() {
    return `The ${this.make} engine has started.`;
  }

};

const Tesla = Object.create(Car);
Tesla.make = "Tesla";
Tesla.year = 2025;
Tesla.batteryLife = "500 miles";

console.log(Tesla.startEngine()); // Inherited method
console.log(`Battery life: ${Tesla.batteryLife}`); // Own property

Here, Tesla is created with Car as its prototype. This means Tesla inherits the startEngine method from Car. We override the make and year properties to reflect Tesla’s identity and add a new property, batteryLife. When we call Tesla.startEngine(), JavaScript looks up startEngine on Tesla. Since it’s not directly on Tesla, it checks the prototype (Car) and finds it there, allowing Tesla to use the method without redefining it.

Adding Properties and Methods to Prototypes

When working with objects created via constructor functions or Object.create(), you can add shared methods and properties to their prototype so all derived objects share the same behavior without duplicating code.

Suppose we want to create a constructor function CarConstructor and add a shared method to its prototype.

function CarConstructor(make, year) {
  this.make = make;
  this.year = year;
}

CarConstructor.prototype.startEngine = function () {
  return `The ${this.make} engine roars to life!`;
};

const ford = new CarConstructor("Ford", 2018);
console.log(ford.startEngine());

In this example, the startEngine method is added to CarConstructor.prototype. Any object created with new CarConstructor() automatically inherits this method. Calling ford.startEngine() works because JavaScript follows the prototype chain to find it.

Overriding Prototype Methods in Child Objects

Sometimes you want to customize or replace a method inherited from a prototype. JavaScript allows you to override prototype methods by simply defining a method with the same name directly on the child object.

Using the previous Car example, let’s override startEngine in a new object Tesla.

const Car = {

  make: "Generic",
  year: 2000,
  startEngine() {
    return `The ${this.make} engine has started.`;
  }

};

const Tesla2 = Object.create(Car);

Tesla2.make = "Tesla";

Tesla2.startEngine = function () {
  return `The ${this.make}'s silent electric engine hums softly.`;
};

console.log(Tesla2.startEngine()); // Overridden method

Here, although Tesla2 inherits from Car, it replaces the inherited startEngine method with its own version. Now, calling Tesla2.startEngine() executes this new method, demonstrating how overrides work.

Using Constructor Functions and Prototypes for Inheritance

Before ES6 classes, constructor functions paired with prototypes were the main way to implement inheritance in JavaScript. Constructor functions create new objects, and shared methods are stored on their prototype.

Let’s create an Animal constructor with a shared method.

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

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

const dog = new Animal("Kol");
console.log(dog.speak());

This code creates an Animal object named dog. The speak method is shared among all Animal instances, kept on the prototype to avoid duplication.

Setting Up Inheritance Between Constructor Functions

Inheritance between constructor functions requires setting the prototype of the child constructor to an instance of the parent’s prototype. This creates the prototype chain.

Let’s create a Dog constructor that inherits from Animal.

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

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

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

const bulldog = new Dog("Bruno", "Bulldog");
console.log(bulldog.speak()); // Inherited method
console.log(bulldog.bark());  // Child method

Here, Dog calls Animal’s constructor to set name. Then we set Dog.prototype to inherit from Animal.prototype. After that, we fix the constructor property to point back to Dog. Now, bulldog can access both speak from Animal and bark defined on Dog.

Calling Parent Prototype Methods in Child Objects

Sometimes a child method needs to reuse the parent’s method. This can be done by explicitly calling the parent prototype’s method with the child’s context.

In the Dog example, let’s override speak but call the parent method inside it.

Dog.prototype.speak = function () {
  const parentSpeak = Animal.prototype.speak.call(this);
  return `${parentSpeak} Also, ${this.name} wags its tail.`;
};

console.log(bulldog.speak());

This code overrides speak in Dog. It first calls the parent’s speak using call(this) to keep the right context, then adds additional behavior. The output combines both messages, showing method reuse.

Prototype Chain Exploration and Property Lookup

JavaScript looks for properties and methods starting from the object itself, then up the prototype chain until it finds the property or reaches the end (null).

Let’s create a deeper chain: AnimalDogBulldog.

function Bulldog(name) {
  Dog.call(this, name, "Bulldog");
}

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

Bulldog.prototype.snore = function () {
  return `${this.name} snores loudly. Zzz!`;
};

const max = new Bulldog("Max");
console.log(max.bark());    // From Dog
console.log(max.speak());   // Overridden in Dog
console.log(max.snore());   // From Bulldog

max has access to methods from all levels. When max.bark() is called, JavaScript looks on max, then Bulldog.prototype, then Dog.prototype and finds bark. This demonstrates how the prototype chain directs property lookup.

Fun Example: Building a Magical Creature Prototype Chain

To illustrate prototypical inheritance in a playful way, let’s create a chain of magical creatures.

const Creature = {

  type: "Creature",
  describe() {
    return `A mysterious ${this.type} wanders the forest.`;
  }

};

const Elf = Object.create(Creature);
Elf.type = "Elf";
Elf.castSpell = function () {
  return `${this.type} casts a shimmering light spell.`;
};

const HighElf = Object.create(Elf);
HighElf.type = "High Elf";
HighElf.castSpell = function () {
  return `${this.type} casts an ancient powerful spell.`;
};

console.log(Creature.describe());
console.log(Elf.describe());
console.log(Elf.castSpell());
console.log(HighElf.describe());
console.log(HighElf.castSpell());

Here, Elf inherits from Creature, and HighElf inherits from Elf. Each level can override methods like castSpell to create unique behavior. This example shows how prototypical inheritance can model real-world hierarchies with shared and specialized behaviors.

Conclusion

Prototypical inheritance is the foundation of JavaScript’s object model, allowing objects to inherit properties and methods directly from other objects. Using Object.create(), constructor functions, and prototype chains, you can set up inheritance that shares functionality efficiently while allowing overrides and extensions. Understanding this model helps unlock the full power of JavaScript objects and their dynamic behavior.

By practicing with real examples, such as creating vehicles, animals, or magical creatures, you see prototypical inheritance in action and appreciate its flexibility and expressiveness.

References

If you want to learn more about JavaScript prototypical inheritance and related topics, these resources are excellent starting points:

Scroll to Top