JavaScript tutorials > Object-Oriented JavaScript > Classes and Prototypes > How do you inherit properties in JavaScript?

How do you inherit properties in JavaScript?

Inheritance in JavaScript is a powerful mechanism that allows you to create new objects based on existing ones, inheriting their properties and methods. This promotes code reusability and helps organize your code in a more maintainable way. This tutorial explores different approaches to inheritance, focusing on prototypal inheritance and class-based inheritance introduced in ES6.

Prototypal Inheritance: The Foundation

This example demonstrates prototypal inheritance using constructor functions and the `Object.create()` method. First, we define an `Animal` constructor with a `name` property and a `sayName` method added to its prototype. Then, we define a `Dog` constructor that calls the `Animal` constructor using `Animal.call(this, name)` to initialize the `name` property. Crucially, `Dog.prototype = Object.create(Animal.prototype)` creates a new object that inherits from `Animal.prototype` and assigns it to `Dog.prototype`. This establishes the inheritance relationship. Finally, we reset `Dog.prototype.constructor` to point back to the `Dog` constructor and add a `bark` method to `Dog.prototype`. The `instanceof` operator checks if an object is an instance of a constructor and also if it's an instance of any of its parent constructors.

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

Animal.prototype.sayName = function() {
  return 'My name is ' + this.name;
};

function Dog(name, breed) {
  Animal.call(this, name); // Call the parent constructor
  this.breed = breed;
}

// Set up the prototype chain: Dog inherits from Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Reset constructor property

Dog.prototype.bark = function() {
  return 'Woof!';
};

const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.sayName()); // Output: My name is Buddy
console.log(myDog.bark());    // Output: Woof!
console.log(myDog instanceof Animal); //true
console.log(myDog instanceof Dog); //true

ES6 Classes: Syntactic Sugar

ES6 classes provide a more convenient syntax for prototypal inheritance. The `class` keyword defines a class, and the `extends` keyword specifies the class to inherit from. The `super()` method calls the parent class's constructor. Underneath the hood, ES6 classes still use prototypal inheritance; they just provide a cleaner and more familiar syntax for developers coming from other object-oriented languages. This allows easier organization of code.

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

  sayName() {
    return `My name is ${this.name}`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call the parent constructor
    this.breed = breed;
  }

  bark() {
    return 'Woof!';
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.sayName()); // Output: My name is Buddy
console.log(myDog.bark());    // Output: Woof!
console.log(myDog instanceof Animal); //true
console.log(myDog instanceof Dog); //true

Concepts behind the snippet

The key concept behind inheritance is code reusability. Instead of rewriting common functionality in each class, you can define it once in a parent class and have child classes inherit it. This reduces redundancy and makes your code more maintainable. Understanding the prototype chain is crucial for grasping how inheritance works in JavaScript. The prototype chain is a series of objects that are linked together, where each object inherits properties and methods from the object in its prototype. Every object in JavaScript (except the root object) has a prototype, and that prototype is also an object, and it has a prototype of its own, and so on. This continues until we reach an object that has null as its prototype. The end of this chain of prototypes is known as the null prototype, and all objects ultimately inherit from it.

Real-Life Use Case Section

Consider a scenario where you are building a game with different types of characters. You might have a base `Character` class with properties like `health`, `attackPower`, and `move`. Then, you can create subclasses like `Warrior`, `Mage`, and `Archer` that inherit from `Character` and add their own unique properties and methods. For example, `Warrior` might have a `specialAttack` method, while `Mage` might have a `castSpell` method. In web applications, consider a `Button` component. You can have a base `Button` class, and then subclasses for specific button types like `PrimaryButton`, `SecondaryButton`, etc., each inheriting the basic button functionality but with different styling.

Best Practices

  • Favor Composition over Inheritance: In some cases, composition (where objects contain other objects as properties) can be a more flexible alternative to inheritance. This avoids the tight coupling that can sometimes arise with inheritance.
  • Keep Inheritance Hierarchies Shallow: Deep inheritance hierarchies can be difficult to understand and maintain. Try to keep your inheritance trees relatively shallow.
  • Use `super()` correctly: When overriding methods in subclasses, make sure to call `super()` if you want to execute the parent class's implementation as well.
  • Understand the Prototype Chain: A solid understanding of the prototype chain is essential for working with inheritance in JavaScript.

Interview Tip

Be prepared to explain the difference between prototypal inheritance and classical inheritance (as found in languages like Java or C++). Also, be ready to discuss the pros and cons of inheritance and when composition might be a better choice. A common interview question is to implement inheritance using both prototypal inheritance and ES6 classes.

When to use them

Use inheritance when you have a clear 'is-a' relationship between objects. For example, a `Dog` 'is-a' `Animal`. Also, use inheritance when you want to reuse code and avoid duplication. When the relationship is more of 'has-a' instead of 'is-a' consider using composition, where one object contains another.

Memory footprint

Inheritance can potentially increase the memory footprint, especially with deep hierarchies. Each object inherits properties and methods from its parent classes, which can add to the object's size. However, methods are typically shared via the prototype, so they don't contribute significantly to individual object sizes. Carefully consider the complexity of your inheritance hierarchies and whether the benefits of code reuse outweigh the potential memory overhead. Modern JavaScript engines are generally efficient in handling inheritance, but it's still a factor to be aware of, especially in resource-constrained environments.

Alternatives

Besides inheritance, you can use:

  • Composition: Combining objects through properties instead of inheritance.
  • Mixins: Functions that add properties and methods to an object.
  • Functional Programming: Using pure functions and avoiding state changes, which can simplify code and reduce the need for inheritance.
Composition is generally preferred over inheritance because it leads to more flexible and maintainable code.

Pros

  • Code Reusability: Avoids code duplication by inheriting properties and methods.
  • Organization: Helps organize code into a hierarchical structure.
  • Maintainability: Changes in the parent class can be easily propagated to child classes.

Cons

  • Tight Coupling: Can create tight coupling between parent and child classes, making it difficult to modify the parent class without affecting child classes.
  • Complexity: Deep inheritance hierarchies can be complex and difficult to understand.
  • The 'Fragile Base Class' Problem: Changes to the base class can unintentionally break derived classes.

FAQ

  • What is the prototype chain?

    The prototype chain is a mechanism in JavaScript where objects inherit properties and methods from other objects. Each object has a prototype, and that prototype also has a prototype, and so on, until reaching `null`. When trying to access a property or method of an object, JavaScript first looks for it on the object itself. If it's not found, it looks at the object's prototype, then the prototype's prototype, and so on, up the chain.
  • How do I prevent inheritance?

    In JavaScript, there's no direct way to completely prevent inheritance for existing object structures. However, you can achieve a similar effect by:
    • Object.freeze(): This method freezes an object, preventing new properties from being added and existing properties from being modified or deleted. However, properties that are objects themselves can still be modified.
    • Object.seal(): This method seals an object, preventing new properties from being added or existing properties from being deleted. Existing properties can still be modified.
    These methods prevent modifications, which indirectly reduces the potential for unintended side effects related to inheritance.
  • Why is `Dog.prototype.constructor = Dog` necessary?

    When you set `Dog.prototype = Object.create(Animal.prototype)`, you are replacing the original `Dog.prototype` object with a new object that inherits from `Animal.prototype`. The original `Dog.prototype` object had its `constructor` property pointing to the `Dog` function. The new `Dog.prototype` object inherits its `constructor` property from `Animal.prototype`, so it now points to the `Animal` function. Setting `Dog.prototype.constructor = Dog` resets the `constructor` property to point back to the `Dog` function, which is important for accurately determining the constructor of `Dog` instances.