Prototype Inheritance as an Object-Oriented Technique in JavaScript
Object-oriented programming (OOP) is one of the core principles of modern software development. By promoting code reusability, maintainability, and scalability, it has become a widely adopted paradigm among developers.
JavaScript has gained popularity due to its dynamic and flexible nature. However, to fully leverage object-oriented programming in JavaScript, it’s essential to understand how prototype inheritance works. This article provides a comprehensive explanation of prototype inheritance in JavaScript, along with practical examples. By learning how prototype inheritance works, you'll gain a clearer and more efficient way to apply object-oriented techniques in your JavaScript code.
What Is Object-Oriented Programming (OOP)?
Object-oriented programming (OOP) is one of the methodologies used in software development.
It wasn’t part of the earliest programming approaches. Instead, it emerged as a response to challenges that arose in software development. Initially, most software was written using a procedural programming approach, which organizes code as a series of sequential instructions. However, as applications grew larger, developers needed more reusable, maintainable, and scalable code structures.
Procedural programming made it difficult to modularize code, and reusing existing code was often impractical. Any time new functionality was added or existing features changed, it usually required modifying large portions of code, making maintenance a challenge.
OOP was developed to address these problems.
Before we explore OOP in depth, let’s briefly review procedural programming—sometimes also referred to as imperative or linear programming.
Procedural Programming
Procedural programming is a coding paradigm in which programs are organized as a series of steps or procedures that the computer follows in order. In this approach, developers typically write functions that operate on data directly.
Here’s a simple example of procedural programming related to a car:
let carName = "Porsche";
let carModel = "911 Targa";
let carColor = "white";
const startCar = name => {
console.log(`${name} is starting`);
};
const helloCar = (name, model, color) => {
const hello = `My car's name is ${name}, the model is ${model}, and the color is ${color}.`;
console.log(hello);
};
const driveCar = name => {
console.log(`${name} is driving`);
};
// Using the functions
startCar(carName);
helloCar(carName, carModel, carColor);
driveCar(carName);
In the example above, the code follows a procedural structure. Each function performs a specific task and is called directly when needed.
Object-Oriented Programming (OOP)
As mentioned earlier, procedural programming has limitations when it comes to reusability and maintainability. OOP was introduced to address those issues.
With OOP, code is structured around real-world concepts. It encourages modular, reusable, and extendable designs by modeling software components as objects.
Let’s return to the car example. A car in the real world has attributes (like engine, wheels, color) and behaviors (such as driving, stopping, or turning). In OOP, these are modeled as properties and methods of an object.
In short, object-oriented programming is a way of modeling real-world entities and concepts in code.
Objects group together both data and behaviors into a single structure.
Below is a refactored version of the previous example using OOP principles with JavaScript’s prototype-based inheritance:
// Car constructor function
function Car(name, model, color) {
this.name = name;
this.model = model;
this.color = color;
}
// Define methods on the Car prototype
Car.prototype.start = function() {
console.log(`${this.name} is starting`);
};
Car.prototype.introduce = function() {
const message = `My car's name is ${this.name}, the model is ${this.model}, and the color is ${this.color}.`;
console.log(message);
};
Car.prototype.drive = function() {
console.log(`${this.name} is driving`);
};
// Example usage
let myCar = new Car("Porsche", "911 Targa", "white");
myCar.start(); // Output: "Porsche is starting"
myCar.introduce(); // Output: "My car's name is Porsche, the model is 911 Targa, and the color is white."
myCar.drive(); // Output: "Porsche is driving"
Compared to the procedural version, this object-oriented code is easier to understand and manage.
Functions like start()
, hello()
, and drive()
are now associated with the myCar
object. That means instead of calling unrelated functions, you're calling methods that clearly belong to a specific object — e.g., myCar.start()
.
An object is essentially a container that combines related data and the functions that operate on that data. For example, a car object contains information like color and model, and it can perform actions like accelerate or stop. By grouping both data and behavior into one structure, OOP helps simplify code and improve readability.
This encapsulated structure also enhances interface clarity, improves maintainability, supports scalability, and promotes cleaner architecture.
Now that we’ve seen the value of OOP, let’s dive deeper into one of JavaScript’s core mechanisms for implementing it: prototype inheritance.
JavaScript Prototype Inheritance
What is prototype inheritance?
Prototype inheritance means that properties and methods owned by a parent (or ancestor) object are inherited by a child (or descendant) object through the prototype mechanism. Let’s explore what prototype inheritance means with the following example:
// Parent object declaration
function Animal() {
this.name = "Animal"; // Property owned by the parent object (Animal)
}
// Adding a method to the parent object (Animal) via the prototype
Animal.prototype.say = function() {
console.log("I am an animal.");
};
// Child object declaration
function Dog() {
this.name = "Dog";
}
// Child object (Dog) inherits properties and methods from the parent object (Animal) using the prototype
Dog.prototype = new Animal();
// Using the parent’s properties and methods from the child (Dog) object
const myDog = new Dog();
console.log(myDog.name); // Output: "Dog"
myDog.say(); // Output: "I am an animal."
This example clearly demonstrates prototype inheritance.
- First, a parent object called
Animal
is declared, with a propertyname
. - Using
Animal.prototype
, a methodsay()
is added to the parent object. This method logs the message"I am an animal."
. - Next, a child object called
Dog
is declared, which needs to inherit from the parentAnimal
. - By setting
Dog.prototype
to an instance ofAnimal
, the child's prototype chain links to the parent, allowing it to inherit properties and methods. - Creating an instance
myDog
ofDog
allows accessingmyDog.name
which outputs"Dog"
, showing that the child’s own property is used. - Calling
myDog.say()
invokes the inherited method fromAnimal.prototype
, printing"I am an animal."
.
Through this mechanism, prototype inheritance allows child objects to share properties and methods defined on parent objects.
What is the Prototype Concept?
What does "prototype" mean?
In dictionary terms, it means "the original form or model." In JavaScript, the term “prototype” refers to the template or blueprint used to create objects — essentially the original object from which other objects inherit.
You can think of it like a car blueprint. The blueprint defines the common features and functionalities every car should have. Using this blueprint, many cars can be manufactured, all sharing the defined features.
Similarly, a prototype is the blueprint used when creating objects. It defines the common properties and methods that those objects should share. In JavaScript, every object has its own prototype, and through that prototype, it inherits shared properties and methods.
For example, when creating multiple car objects, you can define one prototype that contains the shared attributes and behaviors, and then generate each car object based on it. Each car object will share the properties and methods defined on the prototype.
Using prototypes increases code reuse and efficiency because you don’t need to redefine the same properties and methods for each object—inheritance from the prototype handles that.
JavaScript implements this prototype concept through the prototype
property and several related internal properties.
The prototype
Property
The prototype
property in JavaScript is a property of constructor functions that stores properties and methods to be shared among instances created by that constructor.
It is often simply referred to as "the prototype". Every object created by a constructor function inherits from that constructor's prototype
object, enabling shared access to the properties and methods defined there.
How to Use the prototype
Property
In JavaScript, objects have a prototype
property which points to their prototype object. Properties and methods defined on this prototype object are shared among all objects inheriting from it.
For example, consider a constructor function Animal
and an object dog
created from it. The object dog
inherits from Animal.prototype
.
function Animal(name) {
this.name = name;
}
// Method shared by all objects created by Animal constructor
Animal.prototype.speak = function() {
console.log(`Hello, my name is ${this.name}.`);
};
const dog = new Animal("Buddy");
dog.speak(); // Output: "Hello, my name is Buddy."
In this example, Animal.prototype.speak
is a method that all objects created by the Animal
constructor inherit. The dog
object accesses speak()
via its prototype chain.
Thus, you can access or define prototype properties and methods using the syntax ConstructorName.prototype
.
Understanding the role of the prototype
property is essential to mastering JavaScript’s prototype-based inheritance.
Object Literals and Object.create()
Up to this point, we’ve explored prototype-based inheritance using constructor functions. However, JavaScript being a prototype-based language, offers multiple ways to implement inheritance—not just through constructors.
One common and powerful alternative is using the Object.create()
method. This method allows you to create a new object and directly specify another object to be its prototype. This enables inheritance without the need for constructor functions.
In this section, we’ll look at how inheritance can be achieved using object literals together with Object.create()
.
const parent = {
sayHello: function() {
console.log("Hello!");
}
};
const child = Object.create(parent);
child.sayHello(); // Output: "Hello!"
In the example above:
- The
parent
object is defined using an object literal and contains a methodsayHello()
. - The
child
object is created usingObject.create(parent)
, which setsparent
as the prototype of child. - As a result,
child
inherits thesayHello()
method and can call it even though it is not defined directly onchild
.
This approach is especially useful when you want to create simple prototype chains without the overhead or structure of constructor functions or ES6 classes.
By leveraging Object.create()
, you can establish a clean and straightforward inheritance structure between objects using literals, making the prototype chain explicit and easy to follow.
Classes and the extends
Keyword
Starting with ECMAScript 2015 (ES6), JavaScript introduced the class
syntax and the extends
keyword.
You can now define objects using classes, and use extends
to create subclasses that inherit from other classes.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal { // Inherits from the Animal class using extends
constructor(name, breed) {
super(name); // Call the constructor of the parent class
this.breed = breed;
}
bark() {
console.log(`${this.name} barks.`);
}
}
const myDog = new Dog("Buddy", "Retriever");
myDog.speak(); // Output: "Buddy makes a sound."
myDog.bark(); // Output: "Buddy barks."
In the example above:
- The
Animal
class defines a constructor that sets thename
property and a methodspeak()
that logs a message. - The
Dog
class usesextends
to inherit from theAnimal
class. - The
Dog
class defines its own constructor and usessuper()
to call the constructor of theAnimal
class. - A new method
bark()
is added to the Dog class. - The
myDog
instance ofDog
has access to bothspeak()
from the parent class andbark()
from its own class.
This class-based syntax provides a cleaner and more familiar way to implement inheritance, especially for developers coming from object-oriented languages like Java, C++, or Python.
Conclusion
In this article, we explored the concept of prototype inheritance in JavaScript—one of the core features of the language and a powerful tool in object-oriented programming.
We began by understanding what prototypes are and how the prototype chain works. We examined how inheritance can be implemented using constructor functions and prototypes. Then, we looked at alternative approaches to inheritance, including using object literals with Object.create()
, and the modern class-based syntax introduced in ES6 with the extends
keyword.
Prototype inheritance is essential for understanding and effectively applying object-oriented principles in JavaScript. It enables code reuse, improves maintainability, and allows for clear relationships between objects. Mastering prototype-based inheritance not only helps you write more efficient and scalable code, but also deepens your understanding of how JavaScript works under the hood.
With a solid grasp of prototype inheritance, you'll be better equipped to design robust applications and tackle more complex development challenges with confidence.