Back to JavaScript
2026-01-238 min read

Inheritance and the prototype chain (JavaScript)

Learn Inheritance and the prototype chain (JavaScript) step by step with clear examples and exercises.

Why This Matters

In this comprehensive lesson, we will delve into the intricate world of inheritance and prototype chain in JavaScript, which are fundamental concepts for understanding object-oriented programming (OOP) in this versatile scripting language. By the end of this tutorial, you'll have a profound understanding of these concepts, enabling you to write more efficient, reusable, and maintainable code.

Why This Matters

Understanding inheritance and prototype chain is crucial for several reasons:

  1. Code reusability: Inheritance allows us to create new objects based on existing ones, reducing the need to duplicate code. By leveraging a well-designed inheritance hierarchy, we can build complex applications more efficiently.
  2. Object-oriented programming (OOP): OOP is a powerful paradigm that helps structure complex applications more efficiently and maintainably. Mastering inheritance and prototype chain will make it easier for you to implement OOP principles in your JavaScript projects.
  3. Debugging real-world issues: Familiarity with these concepts will help you troubleshoot and fix common problems in JavaScript projects, making you a more effective developer.
  4. Preparing for interviews: Interviewers often test candidates' understanding of inheritance and prototype chain, so mastering these topics can give you an edge when applying for jobs or during technical interviews.

Prerequisites

Before diving into the core concept, it's essential to have a good grasp of the following JavaScript concepts:

  1. Variables, functions, and data types
  2. Objects and properties
  3. Basic control structures (if-else, for, while)
  4. Arrays
  5. Functions as first-class citizens
  6. ES6 features like arrow functions, template literals, and destructuring assignments
  7. Understanding the difference between let, const, and var
  8. Closures and lexical scoping
  9. Promises and async/await
  10. Modules and import/export statements

Core Concept

Inheritance in JavaScript

In JavaScript, inheritance is achieved by creating new objects based on existing ones using the Object.create() method or constructors. When we create a new object with Object.create(prototype), the new object inherits properties and methods from the provided prototype object.

// Creating a prototype object
const personPrototype = {
name: 'John Doe',
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};

// Creating an object that inherits from the prototype
const john = Object.create(personPrototype);
john.greet(); // Output: Hello, I'm John Doe

The Prototype Chain

Every JavaScript object has a hidden property called __proto__, which points to its prototype object. This creates a chain of objects, where each object's prototype is another object in the chain, eventually leading to the root Object prototype. This chain is known as the prototype chain.

console.log(john.__proto__ === personPrototype); // true
console.log(personPrototype.__proto__ === Object.prototype); // true

When we access a property on an object, JavaScript first looks for that property on the object itself. If it's not found, it moves up the prototype chain until it finds the property or reaches the root Object prototype.

Constructors and Prototypes

Constructors are functions that create new objects with a specific structure. By default, when we call a constructor with new, JavaScript creates an empty object and sets its prototype to the constructor's prototype property.

function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};

const john = new Person('John Doe');
john.greet(); // Output: Hello, I'm John Doe

In this example, Person is a constructor that creates objects with a name property and a greet() method. When we call new Person('John Doe'), JavaScript creates an empty object, sets its prototype to the Person.prototype object, assigns the provided name to the new object's name property, and returns the new object.

Classes in ES6

ES6 introduced a syntax that makes it easier to write class-like code using constructor functions and the class keyword. Although classes are syntactical sugar over constructor functions, they still rely on prototypes under the hood.

class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, I'm ${this.name}`);
}
}
const john = new Person('John Doe');
john.greet(); // Output: Hello, I'm John Doe

In this example, we define a Person class with a constructor and a method. When we call new Person('John Doe'), JavaScript creates an empty object, sets its prototype to the Person.prototype object, assigns the provided name to the new object's name property, and returns the new object.

Worked Example

Let's create a simple inheritance hierarchy for a shape application:

// Base Shape constructor
function Shape() {}
Shape.prototype.area = function() {
throw new Error('Must be overridden by subclass');
};

// Rectangle constructor inheriting from Shape
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.area = function() {
return this.width * this.height;
};

// Square constructor inheriting from Rectangle
function Square(side) {
this.side = side;
}
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square;
Square.prototype.area = function() {
return this.side * this.side;
};

In this example, we have a Shape constructor with an abstract area() method. We then create a Rectangle constructor that inherits from Shape, overriding the area() method to calculate the area of a rectangle. Finally, we create a Square constructor that inherits from Rectangle, also overriding the area() method to calculate the area of a square.

const rectangle = new Rectangle(4, 5);
console.log(rectangle.area()); // Output: 20

const square = new Square(5);
console.log(square.area()); // Output: 25

Common Mistakes

  1. Forgetting to call super(): In some cases, it's necessary to call the superclass constructor in the subclass constructor. If you forget to do this, properties of the superclass might not be initialized correctly.
  2. Modifying prototype objects directly: Modifying prototype objects directly can lead to unintended side effects, as multiple objects may share the same prototype and changes will affect all of them. Instead, create new objects with the desired structure or override methods on individual objects.
  3. Not understanding the prototype chain: Understanding how JavaScript searches for properties in the prototype chain is crucial for debugging issues related to inheritance.
  4. Misusing __proto__: While it's possible to manipulate an object's __proto__ property, it's generally not recommended as it can lead to unintended consequences and is less maintainable than using Object.create() or constructors.
  5. Not properly initializing properties in the constructor: If you don't initialize properties in the constructor, they will be undefined, which may cause unexpected behavior when accessing them.
  6. Creating circular references: Creating objects with circular references (where object A's prototype is object B and object B's prototype is object A) can lead to memory leaks and errors.
  7. Using Object.create() without setting the constructor property: When using Object.create(prototype), it's essential to set the constructor property on the new object to ensure that the correct constructor function is called when creating new instances.
  8. Not properly handling the this keyword: In JavaScript, the this keyword can be tricky to handle, especially in asynchronous functions and event handlers. It's important to understand how this behaves in different contexts to avoid errors.

Practice Questions

  1. Create a constructor for a Circle that inherits from the Shape constructor and calculates the area based on the radius.
  2. Given the following code, why does calling john.greet() throw an error? How can you fix it?
const personPrototype = {
name: 'John Doe',
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};

const john = Object.create(personPrototype);
john.greet(); // TypeError: Cannot read property 'name' of undefined
  1. Explain what happens when you call new Rectangle(4, 5).
  2. Create a constructor for a Triangle that inherits from the Shape constructor and calculates the area based on the base and height.
  3. Given the following code, why does calling john.greet() return an empty string? How can you fix it?
const personPrototype = {
name: 'John Doe',
greet() {
return `Hello, I'm ${this.name}`;
}
};

const john = Object.create(personPrototype);
john.greet(); // ""
  1. How would you create a Person class with an instance method called walk() that logs "The person is walking" to the console?
  2. Given the following code, why does calling square.area() throw an error? How can you fix it?
const Shape = function() {};
Shape.prototype.area = function() {
throw new Error('Must be overridden by subclass');
};

const Square = function(side) {
this.side = side;
};
Square.prototype = Object.create(Shape.prototype);
Square.prototype.constructor = Square;
Square.prototype.area = function() {
return this.side * this.side;
};

const square = new Square(5);
square.area(); // TypeError: Cannot read property 'side' of undefined

FAQ

  1. Why can't I just use classes in JavaScript like in other OOP languages?

JavaScript doesn't have native support for classes in the traditional sense. However, ES6 introduced a syntax that makes it easier to write class-like code using constructor functions and the class keyword.

  1. What is the difference between Object.create() and using a constructor with new?

Both methods create new objects, but they differ in how they set up the prototype chain: Object.create(prototype) creates an object with a specific prototype, while using a constructor with new sets the prototype to the constructor's prototype property.

  1. Why is it important to call super() in a subclass constructor?

Calling super() ensures that properties and methods from the superclass are properly initialized before any custom code in the subclass constructor runs. If you forget to call super(), some properties may not be defined, leading to errors or unexpected behavior.

  1. What happens when we call a method on an object that doesn't exist in its prototype chain?

When we call a method on an object that doesn't exist in its prototype chain, JavaScript searches up the prototype chain until it finds a matching method or reaches the root Object prototype. If it still can't find the method, it throws a ReferenceError.

  1. What is the difference between let, const, and var?

let and const are block scoped variables introduced in ES6, while var is function scoped. let and const allow for redeclaring variables within the same scope, but with var, variables can be globally accessible if declared outside a function.

  1. What is a closure in JavaScript?

A closure is an inner function that has access to its outer (enclosing) function's variables, even after the outer function has returned. Closures are essential for implementing private variables and maintaining state within functions.

  1. Why does JavaScript have a prototype-based inheritance system instead of a class-based one?

JavaScript was designed with a prototype-based inheritance system because it offers more flexibility than a class-based system, allowing for dynamic property addition and removal at runtime. Additionally, prototypes are more efficient in terms of memory usage and performance.

  1. What is the difference between __proto__ and prototype?

__proto__ is an internal property of an object that points to its prototype object, while prototype is a property of

Inheritance and the prototype chain (JavaScript) | JavaScript | XQA Learn