Understanding Prototypal Inheritance in JavaScript: Enhancing Code Efficiency

Ayesha Sahar - Jul 11 '23 - - Dev Community

Introduction

In the world of JavaScript, understanding the concept of prototypal inheritance is crucial for building efficient and scalable code. Prototypal inheritance provides a powerful mechanism for code reuse and extensibility, allowing developers to create objects that inherit properties and methods from other objects. By grasping the intricacies of prototypal inheritance, developers can unlock the full potential of JavaScript and write cleaner, more efficient code.

Let's delve deep into the world of prototypal inheritance, exploring its fundamentals, benefits, and practical applications!

Whether you're a seasoned developer looking to reinforce your understanding or a beginner eager to dive into the world of JavaScript, you will get the knowledge and insights to enhance your code efficiency ;)

Understanding Inheritance in JavaScript

Inheritance is a fundamental concept in JavaScript that allows objects to inherit properties and methods from other objects. It enables code reuse and promotes a modular and extensible code structure. In JavaScript, there are two main types of inheritance: classical inheritance and prototypal inheritance.

Let's explore each type and understand how they differ!

Classical vs. Prototypal Inheritance

Classical Inheritance:

Classical inheritance is a concept commonly found in object-oriented programming languages like Java or C++. In classical inheritance, objects are created based on predefined classes, and the inheritance hierarchy is defined explicitly through class definitions. Objects inherit properties and methods from their parent classes, forming a hierarchical structure. To create a new object, you use the "class" keyword to define a blueprint and then instantiate objects using the "new" keyword.

Prototypal Inheritance:

Prototypal inheritance is a unique feature of JavaScript. Instead of using classes, JavaScript utilizes prototype-based inheritance. In prototypal inheritance, objects are created from existing objects, and they inherit properties and methods directly from their prototypes. Every object in JavaScript has a prototype, which serves as a blueprint for inheritance. When a property or method is accessed on an object, JavaScript checks the object itself, and if not found, it looks up the prototype chain until it finds the desired property or method.

The Prototype Chain

The prototype chain is a crucial concept in prototypal inheritance. It represents the linkage between objects and their prototypes, forming a chain-like structure. When a property or method is accessed on an object, JavaScript first checks if the object itself contains that property or method. If not found, it continues the search in the object's prototype. If still not found, it proceeds further up the prototype chain until the property or method is found or until the end of the chain (where the prototype is null).

Example:

Suppose we have a "Vehicle" object with properties and methods related to vehicles in general. We can create a "Car" object that inherits from the "Vehicle" object and adds specific properties and methods for cars.

// Parent Object: Vehicle
const Vehicle = {
  type: "generic",
  honk: function () {
    console.log("Honk! Honk!");
  },
};

// Child Object: Car
const Car = Object.create(Vehicle);
Car.type = "car";
Car.drive = function () {
  console.log("Vroom! Vroom!");
};

// Usage
console.log(Car.type); // Output: "car"
Car.honk(); // Output: "Honk! Honk!"
Car.drive(); // Output: "Vroom! Vroom!"
Enter fullscreen mode Exit fullscreen mode

In this example, the "Car" object is created using Object.create() and inherits properties and methods from the "Vehicle" object. It adds its own specific properties like "type" and "drive." When we access the properties and methods of the "Car" object, JavaScript first checks the "Car" object itself, and if not found, it looks up the prototype chain to find the desired property or method.

The Basics of Prototypal Inheritance

Prototypal inheritance is a core concept in JavaScript that enables objects to inherit properties and methods from other objects.

Here, we'll explore the basics of prototypal inheritance and how it can be implemented in JavaScript ;)

Creating Objects with Constructors

Objects can be created using constructor functions. A constructor function serves as a blueprint for creating multiple instances of an object. When a constructor function is called with the "new" keyword, a new object is created, and the "this" keyword refers to that newly created object. Properties and methods can be added to the object using the "this" keyword within the constructor function.

Let's take an example of a "Person" object created using a constructor function:

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

// Creating instances of Person
const person1 = new Person("John", 25);
const person2 = new Person("Sarah", 30);

console.log(person1.name); // Output: "John"
console.log(person2.age); // Output: 30
Enter fullscreen mode Exit fullscreen mode

In this example, the "Person" constructor function defines the "name" and "age" properties for a person object. The "new" keyword is used to create two instances of the "Person" object, "person1" and "person2", with different values for the name and age properties.

The Prototype Property

In JavaScript, every function has a special property called "prototype." The "prototype" property is an object that is shared among all instances created from the same constructor function. Properties and methods added to the prototype are accessible by all instances of that object type, allowing for efficient memory usage.

Let's add a "greet" method to the "Person" object using its prototype property:

Person.prototype.greet = function () {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

person1.greet(); // Output: "Hello, my name is John and I am 25 years old."
person2.greet(); // Output: "Hello, my name is Sarah and I am 30 years old."
Enter fullscreen mode Exit fullscreen mode

In this example, the "greet" method is added to the prototype of the "Person" object using the "Person.prototype" syntax. This allows all instances of the "Person" object to access the "greet" method and display their name and age.

Creating Instances with the new Keyword

When the "new" keyword is used to create an instance of an object, several things happen behind the scenes. Firstly, a new empty object is created. Secondly, the prototype property of the constructor function is assigned to the prototype of the newly created object. Finally, the constructor function is called with the "this" keyword pointing to the new object, allowing properties and methods to be added to it.

It's important to note that the "new" keyword is not mandatory for object creation in JavaScript. However, using it provides benefits like setting up the prototype chain correctly and simplifying the creation process.

Extending Objects with Prototypes

In JavaScript, objects can be extended with prototypes to add new methods and properties. This allows for code reuse and the ability to create hierarchies of objects with shared functionality.

Let's explore how to extend objects using prototypes and discuss various techniques to modify existing methods and properties!

Adding Methods and Properties to the Prototype

To add new methods and properties to an object's prototype, we can directly modify the prototype object itself. By adding properties and methods to the prototype, all instances of that object type will have access to them.

Let's consider an example where we want to extend the "Person" object with a new method called "introduce" that introduces the person's name and profession:

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

Person.prototype.introduce = function () {
  console.log(`My name is ${this.name}.`);
};

const person1 = new Person("John");
person1.introduce(); // Output: "My name is John."
Enter fullscreen mode Exit fullscreen mode

In this example, we added the "introduce" method to the prototype of the "Person" object. Now, all instances of the "Person" object can access the "introduce" method and introduce themselves.

Modifying Existing Methods and Properties

Prototypes also allow us to modify existing methods and properties of an object. By modifying the prototype, we can change the behavior of all instances of that object type.

Let's say we want to modify the "introduce" method of the "Person" object to include the person's age:

Person.prototype.introduce = function () {
  console.log(`My name is ${this.name} and I am ${this.age} years old.`);
};

const person2 = new Person("Sarah");
person2.age = 30;
person2.introduce(); // Output: "My name is Sarah and I am 30 years old."
Enter fullscreen mode Exit fullscreen mode

In this example, we modified the "introduce" method to include the person's age. By assigning the "age" property to the "person2" instance, we can now introduce both the name and age.

Inheritance Hierarchies

Prototypal inheritance allows for the creation of inheritance hierarchies, where objects can inherit properties and methods from other objects. By utilizing prototypes, we can establish relationships between objects and enable code reuse.

Let's consider an example where we have a "Teacher" object that inherits from the "Person" object:

function Teacher(name, subject) {
  Person.call(this, name);
  this.subject = subject;
}

Teacher.prototype = Object.create(Person.prototype);
Teacher.prototype.constructor = Teacher;

Teacher.prototype.teach = function () {
  console.log(`I teach ${this.subject}.`);
};

const teacher1 = new Teacher("Emma", "Math");
teacher1.introduce(); // Output: "My name is Emma."
teacher1.teach(); // Output: "I teach Math."
Enter fullscreen mode Exit fullscreen mode

In this example, we created a "Teacher" object that inherits from the "Person" object using the Object.create method. By setting the prototype of the "Teacher" object to an instance of the "Person" object, the "Teacher" object inherits the "introduce" method. We then added a new method called "teach" specific to the "Teacher" object.

Leveraging Prototypal Inheritance for Code Efficiency

Prototypal inheritance in JavaScript offers several benefits in terms of code efficiency. Let's take a look at the key aspects: code reusability and the performance optimization potential of prototypal inheritance!

Code Reusability and DRY Principle

One of the fundamental advantages of prototypal inheritance is code reusability. By using prototypes, we can define common functionality in a single place and have it inherited by multiple objects, reducing code duplication and adhering to the Don't Repeat Yourself (DRY) principle.

Consider a scenario where we have multiple objects representing different types of vehicles: Car, Motorcycle, and Bicycle. Each of these objects may have common properties and methods related to their functionality, such as "startEngine" and "stopEngine."

Instead of defining these methods separately for each object, we can leverage prototypal inheritance to define them once in a common prototype object. All instances of the vehicle objects can then inherit these methods, resulting in cleaner and more efficient code.

function Vehicle() {}

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

Vehicle.prototype.stopEngine = function () {
  console.log("Engine stopped.");
};

function Car() {}
Car.prototype = Object.create(Vehicle.prototype);

function Motorcycle() {}
Motorcycle.prototype = Object.create(Vehicle.prototype);

function Bicycle() {}
Bicycle.prototype = Object.create(Vehicle.prototype);

const car1 = new Car();
const motorcycle1 = new Motorcycle();
const bicycle1 = new Bicycle();

car1.startEngine(); // Output: "Engine started."
motorcycle1.startEngine(); // Output: "Engine started."
bicycle1.startEngine(); // Output: "Engine started."
function Vehicle() {}

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

Vehicle.prototype.stopEngine = function () {
  console.log("Engine stopped.");
};

function Car() {}
Car.prototype = Object.create(Vehicle.prototype);

function Motorcycle() {}
Motorcycle.prototype = Object.create(Vehicle.prototype);

function Bicycle() {}
Bicycle.prototype = Object.create(Vehicle.prototype);

const car1 = new Car();
const motorcycle1 = new Motorcycle();
const bicycle1 = new Bicycle();

car1.startEngine(); // Output: "Engine started."
motorcycle1.startEngine(); // Output: "Engine started."
bicycle1.startEngine(); // Output: "Engine started."
Enter fullscreen mode Exit fullscreen mode

In this example, we created a Vehicle object with common methods for starting and stopping the engine. The Car, Motorcycle, and Bicycle objects inherit these methods by setting their prototypes to the Vehicle prototype.

By reusing the Vehicle prototype's methods, we eliminate code duplication and ensure consistency across different vehicle types.

Performance Optimization

Prototypal inheritance can also contribute to performance optimization in JavaScript. Since methods and properties are defined on the prototype, they are shared among all instances of the object. This leads to memory efficiency and better performance, particularly when dealing with a large number of objects.

Consider a scenario where we have thousands of objects representing employees in an organization. Each employee object may have common properties such as "name," "department," and "salary," as well as methods like "calculateSalary" and "displayInfo."

By using prototypal inheritance, we can define these methods once in the prototype and have them shared across all employee objects. This approach ensures efficient memory usage and faster method access compared to defining the methods separately for each object.

function Employee(name, department, salary) {
  this.name = name;
  this.department = department;
  this.salary = salary;
}

Employee.prototype.calculateSalary = function () {
  // Calculation logic
};

Employee.prototype.displayInfo = function () {
  // Display logic
};

// Creating multiple employee objects
const employee1 = new Employee("John Doe", "Sales", 5000);
const employee2 = new Employee("Jane Smith", "Marketing", 6000);
// ... More employee objects

employee1.calculateSalary(); // Shared method
employee2.displayInfo(); // Shared method
Enter fullscreen mode Exit fullscreen mode

In this example, we define the "calculateSalary" and "displayInfo" methods in the Employee prototype. These methods are then shared across all employee objects, resulting in memory efficiency and improved performance.

By leveraging prototypal inheritance, we can optimize our codebase by reusing methods, reducing memory consumption, and achieving faster execution times.

Conclusion

Prototypal inheritance is a fundamental concept in JavaScript that promotes code reusability, organization, and efficiency. By utilizing prototypes and the inheritance chain, you can create clean, maintainable, and performant code. Understanding prototypal inheritance empowers JavaScript developers to harness the full potential of object-oriented programming in their projects. Embrace this powerful feature, and watch your code efficiency soar as you leverage the benefits of prototypal inheritance in JavaScript.


Let's connect!

Twitter

Github

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .