Understanding Prototypes : The Core of JavaScript's Inheritance Model

Understanding Prototypes : The Core of JavaScript's Inheritance Model

JavaScript objects inherit features from one another through prototypes.

Example:

let car = {
  brand: "Toyota",
  model: "Camry",
  year: 2022,
  drive: function() {
    console.log("The car is driving.");
  }
};

console.log(car);

Explanation:

When we log the object, we see all the properties defined by us as well as a property called [[Prototype]].

Each JavaScript object has a special property called [[Prototype]], which points to another object. In our example, it is pointing to the prototype object of the constructor function 'Object'.

Don't worry about big words like 'constructor function', I will cover them at some point in this article.

Now, you might wonder why it is special ?

It is because of the prototype that you are able to access properties defined in other objects. In our example, we are able to access all bult-in properties defined for objects in JavaScript and its prototype.

Oh, again, prototype? Yes, this forms a chain that continues until we reach the prototype whose prototype is null.

So how the chain looks like ?

Now let's look at how we can access this object prototype what are the methods by which we can access this prototypes :

  1. Using the Object.getPrototypeOf() method : This method returns the prototype of the specified object.

    Example:

     let prototypeOfCar = Object.getPrototypeOf(car);
     console.log(prototypeOfCar);
    
  2. Using the proto property: This property allows direct access to an object's prototype. However, it's important to note that proto is deprecated and should be avoided in production code.

    Example:

     let prototypeOfCar = car.__proto__;
     console.log(prototypeOfCar);
    

Now let's explore how to set a prototype for an object. There are two ways to achieve this:

  1. Using Object.setPrototypeOf() : This method is used to set the prototype of a specified object to another object or null.

     // Define a prototype object
     let vehicle = {
         type: 'vehicle',
         describe: function() {
             console.log(`This ${this.name} is among the most expensive ${this.type} in India.`);
         }
     };
    
     // Create a new object 'bike' without a prototype
     let bike = Object.create(null, {
         name: {
             value: "Ducati Superleggera V4"
         }
     });
    
     // Set the prototype of 'bike' to 'vehicle' using Object.setPrototypeOf()
     Object.setPrototypeOf(bike, vehicle);
    
     // Test the prototype relationship
     bike.describe(); // Output: This Ducati Superleggera V4 is among the most expensive vehicle in India.
    
  2. Using the Object.create() method: This method creates a new object by inheriting the properties of the specified object. The new object inherits the prototype of the object used to create it.

     // Define the vehicle
     let vehicle = {
         type: 'vehicle',
         describe: function() {
             console.log(`This is a ${this.type} of ${this.brand} manufactured in ${this.year}.`);
         }
     };
    
     // Create a new object 'car' based on the 'vehicle'
     let car = Object.create(vehicle);
     car.brand = 'Toyota';
     car.year = 2022;
    
     console.log(car);
     car.describe(); // Output: This is a vehicle of Toyota manufactured in 2022.
    
     // Check if the prototype of 'car' is equal to 'vehicle'
     console.log(Object.getPrototypeOf(car) === vehicle); // Output: true
    
  3. Using a constructor

    In JavaScript, a function has a property called prototype, while in an object we have a property called [[Prototype]]. When we use a function as a constructor, the [[Prototype]] of the object is set as the prototype of the function used as a constructor to create a new object.

     // Define a function 'Car'
     function Car(brand, year) {
       this.brand= brand;
       this.year = year;
       this.describe=function(){
          console.log(`This is a ${this.brand} car manufactured in ${this.year}.`);
       }
     }
    
     // Create a new object 'myCar' using the 'Car' function as constructor
     let myCar = new Car('Toyota', 2022);
    
     // Verify if prototype of myCar is the same as the prototype of Car
     console.log(Object.getPrototypeOf(myCar) === Car.prototype); // Output: true
    

There may exist other methods that can be used to set prototypes in JavaScript, which I have not covered here. You can explore them further on your own.

Inheritance

Through the above examples, enthusiasts of object-oriented programming can recognize that we were implementing inheritance.

So what is inheritance ?

Inheritance enables you to define a class that takes all the functionality from a parent class and allows you to add more.

Classes are basically syntactical sugar over constructor functions.

Example:

// Class declaration
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return this.name + ' makes a noise';
  }
}
// Equivalent constructor function and prototype
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  return this.name + ' makes a noise';
};

In the above example, you can see how the class Animal can also be implemented using a constructor function named Animal.

Lets look at the prototypes of class Animal and its equivalent function Animal:

As you can see prototypes is same. In the prototype object you can see they have speak function and a constructor function as well as object prototype whose [[Prototype]] is null.

In classes and functions we have property called prototype and in object we have property called [[Prototype]].

When you declare a class in JavaScript, the JavaScript engine automatically converts it into constructor function and prototype-based syntax behind the scenes. This allows for a more familiar and concise syntax for developers coming from other object-oriented programming languages. However, it's important to understand that classes in JavaScript are not true classes like those found in languages like Java or C++, but rather a different syntax for achieving similar functionality.

Lets us dive into inheritance example:

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

  sayName() {
    console.log('My name is ' + this.name);
  }
}

// Child class inheriting from parent class
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log('Woof! Woof!');
  }
}

// Create instances
const myAnimal = new Animal('Animal');
const myDog = new Dog('Buddy', 'Golden Retriever');

// Call methods
myAnimal.sayName(); // Output: My name is Animal
myDog.sayName(); // Output: My name is Buddy
myDog.bark(); // Output: Woof! Woof!

// Prototype chain
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true

Let us look at the prototypes of classes and objects declared in above example.

In the above example, you can see how the Dog class inherits from the Animal class. myDog is an instance of the Dog class. The Dog class prototype contains its own functions (including the constructor function) and also inherits from the prototype of the Animal class.

In this way inheritance is achieved in JavaScript by class.

When you create any object in JavaScript, only the object's own properties (i.e., the properties you explicitly define) are stored directly within the object itself. Methods and properties inherited from the object's prototype are not duplicated for each instance; instead, instances of objects reference their prototype for inherited methods and properties. This approach optimizes memory usage and allows for efficient code execution by promoting code reuse through prototype-based inheritance.

I hope you now have a clear understanding of what prototypes are, how we obtain prototypes, how we define prototypes, and how prototypes serve as a mechanism for implementing inheritance in JavaScript.