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 contextsprototype 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:
declared a constant
student
and assigned it a value of an empty object.created a
firstName
property and assigned to it the value of the function's first parameter.created a
lasstName
property and assigned to it the value of the function's second parameter.created a
level
property and assigned to it the value of the function's third parameter.created a
increaseLevel
method which will be used to increase the student level by 100 on each call.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:
We declared a constant
student
and set it toObject.create
function. This function creates an empty object regardless of whatever is passed into it. When an object is passed into theObject.create
function, the object becomes the prototype for the object created withObject.create
. Therefore,
incrementObj
is the prototype for thestudent
object.Next, we declared a constant
incrementObj
and set it to an object that has anincreaseLevel
method.
Notes:
The
increaseLevel
function is the same as that from the modification one. It was just brought out of thestudentDataGenerator
function.Bringing it out of the function means we couldn't access the
student
object again. For this reason, we used thethis
keyword inside the function to refer to anystudent
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.
No new object was created inside the function.
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 ofAnotherGenerator
function.Now, when we call
AnotherGenerator
, it must be preceded with thenew
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 withObject.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.
We assigned the constant
AnotherGenerator
to aclass
.
Aclass
in JavaScript helps to encapsulate object generators (constructors) and functionality (methods) within it.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.We brought back the
increaseLevel
function into theclass
.
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.