JavaScript > Prototypes and Inheritance > Prototype Chain > Prototype inheritance

Understanding Prototype Inheritance in JavaScript

This example demonstrates how prototype inheritance works in JavaScript, allowing objects to inherit properties and methods from their prototypes. We'll cover the creation of objects, setting up the prototype chain, and accessing inherited properties.

Basic Prototype Inheritance

This code demonstrates the core concept of prototype inheritance. We define an Animal constructor with a sayName method attached to its prototype. Then, we define a Dog constructor which 'inherits' from Animal.

Crucially, Dog.prototype = Object.create(Animal.prototype); establishes the inheritance. It creates a new object whose prototype is Animal.prototype and assigns it to Dog.prototype. This means instances of Dog can access methods defined on Animal.prototype. We then need to set the Dog.prototype.constructor to Dog. Finally, we add a method specific to Dog named bark.

The Animal.call(this, name) ensures that the Animal constructor is called in the context of the Dog instance, correctly initializing the name property. The instanceof operator confirms the inheritance relationship.

// Define a constructor function
function Animal(name) {
  this.name = name;
}

// Add a method to the Animal prototype
Animal.prototype.sayName = function() {
  return 'My name is ' + this.name;
};

// Create a new constructor function that inherits from Animal
function Dog(name, breed) {
  Animal.call(this, name); // Call the Animal constructor to set the 'name' property
  this.breed = breed;
}

// Set Dog's prototype to be a new Animal object, establishing the inheritance
Dog.prototype = Object.create(Animal.prototype);

// Set the constructor property back to Dog
Dog.prototype.constructor = Dog;

// Add a method specific to Dog
Dog.prototype.bark = function() {
  return 'Woof!';
};

// Create instances
const animal1 = new Animal('Generic Animal');
const dog1 = new Dog('Buddy', 'Golden Retriever');

// Test the inheritance
console.log(animal1.sayName()); // Output: My name is Generic Animal
console.log(dog1.sayName());    // Output: My name is Buddy (inherited from Animal)
console.log(dog1.bark());       // Output: Woof! (specific to Dog)
console.log(animal1.bark());      // Output: animal1.bark is not a function

console.log(dog1 instanceof Animal); // true
console.log(dog1 instanceof Dog);    // true
console.log(animal1 instanceof Dog); // false

Concepts Behind the Snippet

  • Constructor Functions: These are used to create objects. In JavaScript, any function can be used as a constructor with the new keyword.
  • Prototypes: Every JavaScript function has a prototype property, which is an object. When you create an object using new, the newly created object's internal prototype (__proto__, though not directly accessible in modern JavaScript - use Object.getPrototypeOf()) is set to the constructor function's prototype object.
  • Prototype Chain: When you try to access a property of an object, JavaScript first looks for the property on the object itself. If it doesn't find it, it looks at the object's prototype. If it still doesn't find it, it looks at the prototype's prototype, and so on, up the chain. This chain ends when it reaches Object.prototype, which has null as its prototype.
  • Object.create(): This method creates a new object, using an existing object as the prototype of the newly created object. It's the preferred way to establish inheritance in modern JavaScript as it avoids calling the parent constructor directly when setting up the prototype chain.
  • Animal.call(this, name): Calls the Animal constructor setting the context (this) to the current Dog instance and passing the 'name' argument to the Animal function. This allows the Dog to inherit the properties/logic handled by the Animal constructor

Real-Life Use Case

Prototype inheritance is commonly used in libraries and frameworks to create reusable components. For example, a UI library might define a base Component class with common methods for rendering and handling events. Specific UI elements like Button, TextField, and Dropdown can then inherit from Component, adding their own specific functionality while reusing the common logic. This promotes code reuse and reduces duplication.

Best Practices

  • Use Object.create() for Prototype Assignment: Avoid directly assigning a new instance of the parent class to the prototype. Object.create() is cleaner and avoids potentially unwanted side effects from the parent constructor.
  • Reset the Constructor Property: After setting the prototype, always reset the constructor property to point back to the correct constructor function. This is important for accurate type checking and object instantiation.
  • Understand the Prototype Chain: Be aware of the prototype chain and how property lookup works. This will help you debug inheritance issues more effectively.
  • Consider ES6 Classes: While prototype inheritance is the foundation of JavaScript inheritance, ES6 classes (class keyword) provide a more syntactic sugar for defining inheritance relationships, making the code more readable and maintainable. However, they still rely on prototype inheritance under the hood.

Interview Tip

Be prepared to explain the difference between prototype inheritance and classical inheritance (as found in languages like Java or C++). JavaScript uses prototypal inheritance, where objects inherit directly from other objects. Classical inheritance uses classes, and objects inherit from classes. Also, be ready to explain the prototype chain, how it works, and why it's important.

When to Use Them

Prototype inheritance is fundamental to JavaScript and is used extensively behind the scenes. You'll implicitly use it whenever you create objects and functions. Explicitly managing the prototype chain becomes important when you're designing reusable components or creating complex object hierarchies. Use it when you want to share common functionality between objects of different types.

Memory footprint

With prototypal inheritance, methods defined on the prototype are shared among all instances of the object. This means that each object doesn't need to store its own copy of the methods, resulting in a smaller memory footprint compared to classical inheritance where each object might have its own copy of the methods.

Alternatives

While prototypal inheritance is a core feature of JavaScript, there are alternative patterns and features that can be used to achieve similar goals:

  • Composition: Instead of inheriting behavior, objects can be composed of other objects that provide specific functionality. This can lead to more flexible and maintainable code.
  • Mixins: Mixins are functions that add properties and methods to an object. They can be used to share code between objects without creating a strict inheritance hierarchy.
  • ES6 Classes: Classes provide a more structured way to define objects and their relationships. While they still rely on prototypal inheritance, they can make the code easier to read and understand.

Pros

  • Code Reusability: Prototypal inheritance promotes code reusability by allowing objects to inherit properties and methods from other objects.
  • Memory Efficiency: Methods defined on the prototype are shared among all instances of the object, reducing memory consumption.
  • Dynamic Inheritance: Prototypal inheritance allows for dynamic modification of objects and their prototypes at runtime.
  • Flexibility: Prototypal inheritance is a flexible and powerful mechanism for creating complex object hierarchies.

Cons

  • Complexity: Understanding prototypal inheritance can be challenging for developers unfamiliar with the concept.
  • Debugging: Debugging inheritance issues can be difficult due to the complex relationships between objects and their prototypes.
  • Performance: Accessing properties and methods through the prototype chain can be slower than accessing properties directly on the object.

FAQ

  • What is the prototype chain?

    The prototype chain is a series of links between objects in JavaScript. When you try to access a property on an object, JavaScript first looks for the property on the object itself. If it doesn't find it, it looks at the object's prototype, and so on, up the chain until it reaches Object.prototype. If the property is still not found, it returns undefined.

  • Why is it important to set the constructor property after modifying the prototype?

    Setting the constructor property ensures that the constructor property of the prototype object points back to the correct constructor function. This is important for accurate type checking and object instantiation. For example, instanceof relies on the constructor property to determine the type of an object.

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

    Object.create(Parent.prototype) creates a new object with Parent.prototype as its prototype, without invoking the Parent constructor. Using new Parent() creates a new instance of Parent, which can have side effects from the Parent constructor. Object.create() is generally preferred for setting up inheritance as it's cleaner and avoids unwanted side effects.