Classes In JavaScript

The introduction of classes in JavaScript revolutionized the way developers structure and organize their code. Classes, a key component of object-oriented programming (OOP), provide a clean and intuitive syntax for creating reusable blueprints to define objects with shared properties and behaviors. With their arrival in ECMAScript 2015 (ES6), JavaScript gained a powerful tool that brought familiarity to developers coming from other programming languages. Classes empower developers to embrace concepts like encapsulation, inheritance, and polymorphism, fostering modular and scalable codebases.

In this article, we embark on an exploration of classes in JavaScript, unlocking their potential for building robust applications. We'll delve into the syntax and features that classes offer, understand how they relate to prototypes and objects, and uncover the benefits they bring to the development process. Whether you're new to classes or seeking to deepen your understanding, this guide will equip you with the knowledge and insights to leverage the full capabilities of classes in JavaScript.

Prerequisites:

For this article to be fully understood, the reader has to basic understanding of certain topics in JavaScript. some of which include:

  • objects

  • functions

  • this keyword and its values in different contexts

  • prototype and prototypal inheritance

  • methods in JavaScript

What Are Classes In JavaScript?

Classes are a construct introduced in ECMAScript 2015 (ES6) to provide a more structured and intuitive way of defining objects and their behaviors. A class serves as a blueprint or template for creating objects with shared properties and methods.

Let's assume you want to build a software that displays data about certain students in a university. The data could include the students' first names, last names, and the level the student is currently in. Also, you might want to manipulate the level the student is currently in. Therefore, you will need the following sets of data :

  • first name

  • last name

  • current level

This could be achieved with an object thus:

const studentOne = {
  first: "John",
  last: "Doe",
  level: 100,
  increaseLevel: function () {
    studentOne.level += 100;
  },
};
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 100, increaseLevel: ƒ}
studentOne.increaseLevel();
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 200, increaseLevel: ƒ}

Here, a new student was created with properties such as first name, last name, level, and a function that increases the student's level by 100 whenever it's called.

This serves the purpose of the software entirely as all the data and functionality required for the software are present in the object.

However, it won't take long to notice that this method won't be plausible. This is because the university might have hundreds if not thousands of students. So it wouldn't make sense to start creating individual objects for each student.

Also, another defect of this approach is that it contravenes the Don't Repeat Yourself (DRY) principle for programming.

Modification One

What if we could create a function that would generate these objects for us? So we wouldn't need to start creating each student data object manually. Rather, the function would generate it for us.

const studentDataGenerator = function (firstName, lastName, level) {
  const student = {};
  student.firstName = firstName;
  student.lastName = lastName;
  student.level = level;
  student.increaseLevel = function () {
    student.level += 100;
  };
  return student;
};
const studentOne = studentDataGenerator("John", "Doe", 100);
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 100, increaseLevel: ƒ}
studentOne.increaseLevel();
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 200, increaseLevel: ƒ}

Here, we created a function that generates students' data. Within the function, we:

  1. declared a constant student and assigned it a value of an empty object.

  2. created a firstName property and assigned to it the value of the function's first parameter.

  3. created a lasstName property and assigned to it the value of the function's second parameter.

  4. created a level property and assigned to it the value of the function's third parameter.

  5. created a increaseLevel method which will be used to increase the student level by 100 on each call.

  6. Returned the student object from the function.

This method also works well and is actually a more plausible solution. However, this method also has some problems.

  • Each student's object has the increaseLevel function within it. This function actually takes up space in the computer memory. If the university has a large number of students or if the function contains a long line of code, this function within each object will take up a lot of space and may affect the proper functioning of the app.

  • If the university tries to add a new feature, say decrease the student's level, the software engineer would have to rewrite the decreaseLevel code for all the previous students the function generated.

Modification Two

What if we could create the increaseLevel function elsewhere. So, whenever the student object wants to make use of the function, it will just go there automatically. This solution ensures that each object does not have the increaseLevel function with it, rather, it goes somewhere else for the function whenever it needs it.

const studentDataGenerator = function (firstName, lastName, level) {
  const student = Object.create(incrementObj);
  student.firstName = firstName;
  student.lastName = lastName;
  student.level = level;
  return student;
};
const incrementObj = {
  increaseLevel: function increaseLevel() {
    this.level += 100;
  },
};
const studentOne = studentDataGenerator("John", "Doe", 100);
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 100}
studentOne.increaseLevel();
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 200}

Here, we created the same function as before, but with a slight modification:

  1. We declared a constant student and set it to Object.create function. This function creates an empty object regardless of whatever is passed into it. When an object is passed into the Object.create function, the object becomes the prototype for the object created with Object.create. Therefore,
    incrementObj is the prototype for the student object.

  2. Next, we declared a constant incrementObj and set it to an object that has an increaseLevel method.

Notes:

  • The increaseLevel function is the same as that from the modification one. It was just brought out of the studentDataGenerator function.

  • Bringing it out of the function means we couldn't access the student object again. For this reason, we used the this keyword inside the function to refer to any student object that was called on it.

  • The goal here has been achieved as each student object does not have the increaseLevel function within it. Rather, it has access to it whenever it needs it through prototypal inheritance.

Modification Three

The second modification can cater to all the needs specified. However, what if we could make the code smaller and hide away some functionality while maintaining efficiency?

This can be done using the new keyword. This keyword would help automate some steps in the code, thereby making the code more concise.

const AnotherGenerator = function (firstName, lastName, level) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.level = level;
};
AnotherGenerator.prototype.increaseLevel = function increaseLevel() {
  this.level += 100;
};
const studentOne = new AnotherGenerator("John", "Doe", 100);
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 100}
studentOne.increaseLevel();
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 200}

This is actually the same code as the previous one. It just has some slight modifications.

  1. No new object was created inside the function.

  2. Since all functions in JavaScript are objects and function at the same time, we used the dot notation to add the increaseLevel function to the prototype of AnotherGenerator function.

  3. Now, when we call AnotherGenerator, it must be preceded with the new keyword.

Note:

The new keyword automated three things for us:

  • It created a new variable inside AnotherGenerator function and assigned to it an empty object.
    (We did this by ourselves in the second modification)

  • It also returned the new object out of the function.
    (We also did this by ourselves)

  • It created a link between the newly created object and the increaseLevel function.
    (We also did this by ourselves with Object.create function in the second modification)

Note that AnotherGenerator function is not camelCased. This is a convention among developers. It signifies that the function must be called with the new keyword.

Final Modification

The previous modification achieved all the goals and is suitable for production. However, what if we could encapsulate every data and functionality the object needs into one place?

For this, JavaScript gave us the class and constructor keywords.

const AnotherGenerator = class {
  constructor(firstName, lastName, level) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.level = level;
  }
  increaseLevel = function increaseLevel() {
    this.level += 100;
  };
};
const studentOne = new AnotherGenerator("John", "Doe", 100);
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 100}
studentOne.increaseLevel();
console.log(studentOne);
//{first: 'John', last: 'Doe', level: 200}

The code above is a slight modification of the previous code.

  1. We assigned the constant AnotherGenerator to a class.
    A class in JavaScript helps to encapsulate object generators (constructors) and functionality (methods) within it.

  2. We changed the function keyword to a constructor keyword. This is necessary. classes and constructors go together in JavaScript.
    Constructors are just a function used to create objects within a class.

  3. We brought back the increaseLevel function into the class.

Conclusion

With this, we ended up with a class. We saw firsthand some of the principles of object-oriented programming such as inheritance and encapsulation at play.

Hopefully, we now understand how classes work under the hood in JavaScript. This knowledge helps us successfully predict the behavior of our code and helps in debugging it.