AlgoDaily

Class vs. Prototypical Inheritance

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Introduction

JavaScript, the ubiquitous language of the web, has long been a source of intrigue for developers, particularly when it comes to object-oriented programming (OOP) concepts. While most OOP languages rely on classes as the cornerstone of inheritance, JavaScript takes a unique approach, utilizing prototypes to establish relationships between objects.

This often leads to confusion among developers, as JavaScript does, in fact, have classes. But what exactly is the distinction between class-based inheritance and prototypical inheritance in JavaScript? Delving into this realm will unravel the intricacies of JavaScript's inheritance mechanism and empower developers to master this fundamental concept.

Object Oriented Programming vs JavaScript

Object Oriented Programming vs. JavaScript

In the realm of programming, objects are often described as instances of classes. This textbook definition, while accurate, can leave beginners perplexed. To simplify this concept, imagine classes as blueprints for real-world entities. Just as architects create blueprints for buildings, programmers use classes to design the structure of objects.

JavaScript, despite its widespread use, i*s not an object-oriented language in the traditional sense*. However, it does support object-oriented principles through a mechanism called prototypal inheritance. This concept, while initially confusing, offers greater flexibility and power compared to classical inheritance.

Class and Objects

At the core of prototypal inheritance lies the prototype chain, a hierarchical structure where objects inherit properties and methods from their prototypes. The Object prototype, located at the top of this chain, serves as the foundation for all JavaScript objects.

While ES6 introduced the class keyword, it's important to understand that this is merely syntactic sugar, a way to organize code in a more class-like fashion. JavaScript's underlying prototype-based nature remains unchanged.

To grasp the essence of prototypal inheritance, consider the following analogy: Imagine a family tree, where each person inherits traits and characteristics from their parents. Similarly, in JavaScript, objects inherit properties and methods from their prototypes.

This hierarchical relationship enables code reuse and promotes modularity. Developers can create base objects with common properties and methods, and then create child objects that inherit these traits while adding their own unique characteristics.

Fill In

Objects can be created with the ____ method.

Class-based inheritance and the Weak Base Class problem

Class-based inheritance and the Weak Base Class problem

The introduction of the class keyword in ES6 marked a significant shift in JavaScript, allowing developers to more naturally implement an object-oriented approach. With the extends keyword, JavaScript introduced a streamlined way to inherit properties and methods between classes. However, this approach is not without its challenges, one of which is the Weak Base Class problem.

Consider a scenario with three classes: Animal, Bird, and Penguin. In this hierarchy, Animal is the base class, Bird extends Animal, and Penguin extends Bird. This structure might initially seem straightforward - Penguin inherits from Bird, which in turn inherits from Animal. However, this chain of inheritance can introduce complexity and unexpected behavior, as Penguin implicitly inherits from Animal through Bird.

To better understand this, let's delve into a concrete example:

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

 eat() {
  console.log(`${this.name} is eating.`);
 }
}

class Bird extends Animal {
 constructor(name, kind) {
  super(name);
  this.kind = kind;
 }

 fly() {
  console.log(`${this.name}, a ${this.kind}, is flying.`);
 }
}

class Penguin extends Bird { // Here lies the Weak Base Class problem
 constructor(name) {
  super(name, "penguin");
 }

 swim() {
  console.log(`${this.name} is swimming, not flying.`);
 }
}

In this example, Penguin inherits from Bird, which in turn inherits from Animal. While this inheritance chain seems logical, it can lead to issues if the methods in Animal are not compatible with the behaviors of Penguin. For instance, if Bird has a method fly(), it's not relevant for a Penguin, which can't fly.

To tackle this issue, developers might need to introduce an intermediate class or refactor the existing classes. For example, they could introduce a FlightlessBird class between Bird and Penguin to better represent the behaviors of flightless birds.

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

 eat() {
  console.log(`${this.name} is eating.`);
 }
}

class Bird extends Animal {
 constructor(name, kind) {
  super(name);
  this.kind = kind;
 }

 fly() {
  console.log(`${this.name}, a ${this.kind}, is flying.`);
 }
}

class Penguin extends Bird { // Here lies the Weak Base Class problem
 constructor(name) {
  super(name, "penguin");
 }

 swim() {
  console.log(`${this.name} is swimming, not flying.`);
 }
}

const penguin = new Penguin();
console.log(penguin.fly());
```

Inheritance Chain Leak

Before moving on, pause on the shape of the class hierarchy.

The weak base class problem is not that extends is always wrong. It is that each subclass receives the full behavior chain above it, even when part of that behavior does not fit the real object. In this example, Penguin gets fly() because it extends Bird, not because flying makes sense for penguins.

Inheritance Chain Leak

Inheritance Chain Trap

Before moving on, pause on the shape of the Animal → Bird → Penguin chain.

The issue is not that class is broken. The issue is that a subclass receives the entire behavior contract of its parent chain, even when part of that contract stops making sense. In the diagram, notice how Penguin gets fly() because it extends Bird, not because flying is actually valid for penguins.

Inheritance Chain Trap

Multiple Choice

What is a flaw in class-based inheritance?

Inheriting Properties and Methods with Prototypical Inheritance

Inheriting Properties and Methods with Prototypical Inheritance

Objects are not merely isolated entities; they possess intricate relationships that shape their behavior. One such relationship is prototypal inheritance, a mechanism that allows objects to inherit properties and methods from other objects, forming a hierarchical lineage.

Imagine a grand tapestry woven with threads of interconnectedness, each thread representing an object. This tapestry is the prototype chain, where objects inherit traits from their ancestors, much like children inherit traits from their parents.

To illustrate this concept, let's consider the familiar example of a car and its relationship to the broader category of vehicles:

const car = {
  name: "BMW",
  power: "2000 BHP",
  color: "black"
};

console.log(car);
console.log(car.name);
console.log(car.power);
console.log(car.color);

Examining the car object in the console reveals a link to the Object prototype, the foundation of all JavaScript objects. This connection grants the car access to the vast array of properties and methods inherent in the Object prototype.

Car Object

Now, let's introduce a new object, vehicle, representing the generic concept of a vehicle:

const vehicle = {
  wheels: 4
};

Here, we define a property called wheels with a value of 4, establishing a default value for the number of wheels for any vehicle.

To connect the car to this broader vehicle category, we employ the Object.setPrototypeOf() method:

Object.setPrototypeOf(car, vehicle);

console.log(car.wheels);

By assigning vehicle as the prototype of car, we establish a hierarchical relationship, empowering the car to inherit properties from its vehicle ancestor. Printing the wheels property of the car object now yields the value 4, demonstrating the successful inheritance of this trait.

This mechanism extends to methods as well, as methods in JavaScript are essentially properties of objects. Prototypical inheritance enables objects to inherit the functionality of their ancestors, promoting code reuse and maintainability.

Fill In 2

The prototype of an object can be retrieved using the _____ method

What to use?

What to use?

The main problem with class inheritance is that there is a side effect when inheriting a class from another. Every sub-class inherits properties and methods from the parent class. As mentioned above, this can quickly lead to huge problems if not used properly. This is where the age-old design pattern of composition over inheritance comes into the picture.

When people say prefer composition over inheritance, they are referring to class inheritance. There are a lot of flaws with class-based inheritance, but not all inheritance has these issues. Composition in JavaScript is implemented through concatenative inheritance-- wait, what is that?

Concatenative Inheritance

Concatenative Inheritance

Prototypal inheritance, the cornerstone of JavaScript's object-oriented programming paradigm, is fundamentally rooted in concatenative inheritance. This concept entails merging properties from one object into another, creating a new composite object. Unlike class inheritance, where parent-child relationships dictate the inheritance hierarchy, concatenative inheritance offers greater flexibility and independence.

To illustrate this concept, consider the following code snippet:

const wheels = { wheels: 4 };
const color = { color: "black" };

const Car = () => {
  return Object.assign({}, wheels, color);
};

const car = Car();
console.log(car);

In this example, we define two objects, wheels and color, encapsulating the essential characteristics of a car. The Car function utilizes the Object.assign() method to create a new object, car, by combining the properties of wheels and color. This process exemplifies concatenative inheritance in action.

Now, let's envision a scenario where we want to create a black truck with enhanced power and wheels. We can leverage the existing color object and introduce new properties, such as power and wheels, specifically tailored to the truck:

const power = { power: "3000 BHP" };
const truckWheels = { wheels: 6 };

const Truck = () => {
  return Object.assign({}, color, power, truckWheels);
};

const truck = Truck();
console.log(truck);

The Truck function employs the same concatenative approach, combining the color object with the newly defined power and truckWheels objects. This demonstrates the flexibility of concatenative inheritance, allowing us to create new objects without inheriting any unwanted properties from previous creations.

Concatenative inheritance offers a distinct advantage over class inheritance, providing a more granular approach to object composition. It empowers developers to selectively inherit properties, tailoring objects to specific requirements. This flexibility is particularly valuable in JavaScript, where the lack of class-based inheritance constraints necessitates alternative strategies for object creation.

```javascript
let power = { power: "8000 BHP" }
const Truck = () => {
    return Object.assign({}, power, color, wheels);
}
let blackTruck = Truck();
blackTruck["wheels"] = 6;
console.log(blackTruck);
```

Composition Object Shape

Concatenative inheritance is easier to reason about when you picture it as building a final object from small capability objects.

Instead of asking, “What parent does this object come from?”, ask, “Which pieces are copied into this object?” With Object.assign(), source objects are merged left to right, and later properties can override earlier ones.

Composition Object Shape

Composition Shape

Concatenative inheritance is easier to reason about if you picture it as assembling an object from small capability objects.

Instead of asking, “What parent class should this inherit from?”, ask, “Which properties or behaviors should this object receive?” The visual below contrasts a rigid parent-child chain with a flatter composition model using Object.assign().

Composition Shape

Summing up

Summing up

Prototypical inheritance is usually misunderstood with prototype delegation, but it is more related to concatenative inheritance. It simplifies object creation and sets clear boundaries about what is to be inherited and what is not. It has clear advantages over class inheritance and solves many design flaws of the same. We want to sign off with a famous problem of class inheritance, the Gorilla/Banana problem:

Imagine you simply desire a ripe banana to enjoy. However, when employing class inheritance, you inadvertently inherit not only the banana but also the gorilla holding it and, by extension, the entire jungle surrounding them. This excessive inheritance of unwanted properties can lead to bloated and unwieldy code structures.

To overcome this hurdle, concatenative inheritance provides a more controlled and flexible alternative. By selectively merging desired properties from various sources, concatenative inheritance allows you to obtain the banana without inheriting the entire jungle along with it.

In essence, concatenative inheritance empowers developers to craft lean, maintainable, and reusable code, avoiding the pitfalls of class inheritance and achieving greater precision in object composition.

True or False

Prototypical inheritance is the same as prototype delegation. True or False?

One Pager Cheat Sheet

One Pager Cheat Sheet

  • Objects in JavaScript are based onprototypesrather than classes, so it is important to understand the distinction between class-based and prototypical inheritance.
  • JavaScript utilizes a prototype chain instead of classical inheritance to make it more powerful than traditional Object Oriented Programming.
  • Objects can be created with the Object.create() method, a built-in JavaScript method that is part of the prototype chain, allowing for more flexibility and control over objects used for programming.
  • With the introduction of the class keyword in ES6, developers have been able to create class-based inheritance models; however, the Weak Base Class problem arises when inheriting from a class which itself inherits from a superclass.
  • - Class inheritance can leak unwanted behavior down the chain when a subclass is forced into the wrong parent type.
  • - Class inheritance can accidentally pass down behavior that is structurally valid but semantically wrong.
  • The Weak Base Class problem occurs when a subclass C inherits from not only its intended parent class B, but also from the parent class of B (the base class or superclass A) implicitly, which can lead to unexpected and unpredictable behaviour requiring additional efforts for addressing.
  • Prototypical inheritance allows Objects to inherit properties and methods from their prototypes, looking up the chain if needed.
  • Objects can use prototypes to inherit properties and methods from their parent object through a process known as Prototypical inheritance.
  • In software design, it is generally preferable to use composition over inheritance to achieve concatenative inheritance for better manageability and scalability.
  • Concatenative inheritance allows objects to be created from existing objects without inheriting properties from pre-existing parent-child relationships.
  • - Composition builds objects by merging small parts; there is no required parent-child chain.
  • - Composition builds objects by combining focused pieces instead of inheriting an entire parent chain.
  • Prototypical inheritance simplifies object creation, has clear advantages over class inheritance, and solves the classic "Gorilla/Banana" problem.
  • Prototypical Inheritance involves inheriting methods or features from a prototype, while Prototype Delegation is a built-in language feature of JavaScript in which an object delegates an attribute to another object in its prototype chain.
Open interactive view →