12 minutes read

In Object-Oriented Programming (OOP), a class is a key concept, especially for inheritance. Classes serve as templates for creating objects (instances) in many OOP languages, similar to prototypes in JavaScript. Although JavaScript mainly uses prototypes for inheritance, ECMAScript 2015 (ES6) added support for class-based inheritance by introducing the class keyword and class-like syntax. However, it's important to understand that this support is still implemented using prototypes. That means, when we use classes, behind the scenes, we are still working with prototypes. You can think of it as simplifying the code's structure to make it more familiar and user-friendly for developers to work with object-oriented programming concepts. Let's take a look at the differences between their usage in the following sections.

Prototypes

Prototypes are good old fellows in JavaScript. We use prototyping as the primary method to create templates that define properties and methods for objects to be replicated. You are probably familiar with the concept of prototyping (using object literals) in previous topics. In JavaScript, there is another common practice for establishing prototypal linkage between parent and child objects by using function constructors.

In this topic, we will primarily focus on using function constructors as prototypes. This is because when you use object literals, you need to define each property of each object line by line. This can make your code messy and repetitive, especially when dealing with many similar objects. That's why using function constructors is a better choice. It will also make it easier for us to see how they are similar to 'classes', which we'll explore in the next section.

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    
    this.getFulName = function() {
        return firstName + " " + lastName;
    };
}

Here is our example constructor function named Person, which will behave as an object template for each object we will create based on it. It takes two parameters, firstName and lastName. Inside the constructor function, we use these parameters to define the properties this.firstName and this.lastName to store their values in the created objects. We also create the method this.getFullName() to return the full name by concatenating these values.

Here's an example instance created from this prototype:

let person1 = new Person("John", "Doe");

// Accessing properties
console.log(person1.firstName); // "John"
console.log(person1.lastName);  // "Doe"

console.log(person1.getFullName()); // "John Doe"

In this example, person1 is an instance of the Person object, and it is created by using the new keyword. It has its own firstName and lastName properties, and you can call the getFullName() method on the instance to get their full names.

The 'prototype' property

Let's imagine a scenario where you have to create numerous object instances based on a prototype, and you want to save memory at the same time. However, as the structure of your base object becomes more complex, JavaScript may need to follow the prototype chain to access properties and methods in the created child objects. Therefore, creating a new layer of inheritance, like extending the base object (our constructor function in this case), can be helpful. We can manage this using prototype property, as in the following example:

Person.prototype.greet = function() {
  console.log('Hello, ' + this.firstName + ' ' + this.lastName + '!');
};

Here, we have a greet method outside the function constructor that outputs a greeting message by concatenating firstName and lastName properties. For all instances created from Person to access and share this method, we have denoted the prototype property using dot notation between the constructor function's and the method's name.

person1.greet(); // "Hello, John Doe!"

And now, our instance person1 can call the greet method even though it wasn't defined in the original constructor. This is because the greet method is added to the Person prototype, and all instances of Person share the same prototype methods.

Classes

Classes are the new kids on the block in JavaScript. They have become a more modern and familiar way of defining object templates and establishing inheritance relationships with ES6. This is mostly attributed to the fact that, while prototypes are still essential, classes are viewed as having a more common readability among OOP developers. Aside from that, they both serve the same purpose: to define object templates in order to create instances based on them.

Let's stick with the Person template and check out how we define a class in JavaScript:

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName() {
        return this.firstName + " " + this.lastName;
    }
}

In the code above, we have defined our Person class using the class keyword. Then, inside curly braces, we added a constructor method that accepts firstName and lastName as inputs. The constructor is a special method that is called when you create a new instance (object) based on this class. Then we see getFullName method defined within the class. As you've probably noticed, the syntax and structure are quite similar to using a function constructor with prototypes.

Creating instances based on the class also follows the same syntax:

let person1 = new Person("John", "Doe");

// Accessing properties
console.log(person1.firstName); // "John"
console.log(person1.lastName);  // "Doe"

// Calling the getFullName() method
console.log(person1.getFullName()); // "John Doe"

When you want to extend the class by adding properties or methods, you can use the property method in the same way:

Person.prototype.greet = function() {
  console.log('Hello, ' + this.firstName + ' ' + this.lastName + '!');
};

This is because JavaScript is actually working with prototypes, although it uses class syntax.

Subclasses

In JavaScript, you can create new classes that are similar to existing ones (the parent or superclass) but with extra features or modifications. These specialized types of extensions of classes are called subclasses, and they differ from simply adding extra features through prototyping.

Subclasses can be created in JavaScript using both prototypes and classes, although the steps involved are slightly different. We use the extends keyword to define a subclass in a class, but in a prototype, we manually set up the prototype chain and use child constructors to generate subclasses. You can create subtypes (child constructors or subclasses) that inherit and extend the behavior of their parent classes using either technique.

When you define a new property or new method to the subclass, it means that this method is unique to the subclass itself. It won't be available in instances of the parent class or other subclasses. If you want those methods and properties to be shared among all instances of the parent class and its subclasses, you need to add them to the parent class (class scope).

Let's create a subclass called Teacher based on our parent function constructor and superclass Person respectively:

  • Subclasses (child constructors) with Prototypes
function Teacher(firstName, lastName, subject) {
    // Call the parent constructor
    Person.call(this, firstName, lastName);
    
    // Add child-specific property
    this.subject = subject;
}

Here, we have a child constructor, Teacher, derived from our parent function constructor, Person. Inside the Teacher constructor, we use the call keyword as Person.call(this, firstName, lastName) to make sure that when we create a new Teacher object, it inherits all the properties and setup from the Person constructor. It's like borrowing the Person's basic setup.

Next, we need to set up this prototype chain of inheritance as follows:

Teacher.prototype = Object.create(Person.prototype);
Teacher.prototype.constructor = Teacher;

In the first line, we use Object.create to create a new object that will inherit properties and methods from Person.prototype, and we set this new object as the prototype for the Teacher.prototype object. This connection means that Teacher.prototype will receive features from Person.prototype, so instances of the Teacher class can access properties and methods from both Teacher.prototype and Person.prototype.

In the second line, we're simply telling JavaScript that Teacher is the constructor function for instances created from the Teacher class. This is important when we create new objects (instances) that inherit features from Teacher.prototype and Person.prototype.

  • Subclasses with Classes

When we work with classes and want to create a subclass, we have a simpler syntax. To create a subclass named Teacher based on the superclass (parent class) Person, we use the extends keyword. Within the constructor function, we use super to access and invoke the parent class's constructor.

The extends keyword is used to create a new class (a subclass) from an existing class (the superclass). It allows the new class to draw on the existing class's characteristics. The super keyword denotes the superclass in this case, and it is used within the constructor of a subclass. It invokes the superclass's constructor and allows the subclass to set up its own properties while also accessing the foundational behavior of the superclass.
class Teacher extends Person {
    constructor(firstName, lastName, subject) {
        super(firstName, lastName); // Call the parent class constructor
        this.subject = subject;
    }
}

We don't need any extra line of code to create or complete an inheritance linkage between superclass Person and subclass Teacher since super allows us to establish this relationship between them.

Lastly, if we want to set up prototypes of either this child class constructor or a subclass Teacher, we use the syntax below:

Teacher.prototype.teach = function() {
    console.log(`${this.firstName} ${this.lastName} is teaching ${this.subject}`);
};

// Create instances
const person1 = new Person("John", "Doe");
const teacher1 = new Teacher("Jane", "Smith", "Math");

console.log(person1.getFullName()); // "John Doe"
console.log(teacher1.getFullName()); // "Jane Smith"
teacher1.teach(); // "Jane Smith is teaching Math"

Here, in this code, we define a teach method for the prototype of the Teacher subclass, and it prints the teacher's name and the subject they are teaching. We have also two instances, person1 and teacher1, and their full names are displayed. When you call teacher1.teach(), it prints the message "Jane Smith is teaching Math" in the console.

Conclusion

JavaScript uses both prototypes and the class syntax introduced in ES6 to create object templates in Object-Oriented Programming (OOP). The class syntax promotes OOP familiarity, however, it is primarily based on prototypes.

Prototypes are built-in templates in JavaScript, generally utilizing function constructors, but ES6 classes offer a more organized syntax. Inheritance can be accomplished with either method. We can expand parent constructors and superclasses in more specialized ways, creating subclasses. With prototypes, you need to manually set up the prototype chain and use parent constructors. In contrast, ES6 classes simplify inheritance using the extends keyword and super method, making the process more straightforward and readable.

11 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo