Object-oriented programming and JavaScript classes
OOP is a common programming paradigm supported by most high-level languages. In OOP, you typically program an application using the concept of objects, which can be a combination of data and code.
Data represents information about the object, while code represents attributes, properties, and behaviors that can be carried out on objects.
OOP opens up a whole new world of possibilities as many problems can be simulated or designed as the interaction between different objects, thereby making it easier to design complex programs, as well as maintain and scale them.
JavaScript, like other high-level languages, provides support for OOP concepts, although not fully (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), but in essence, most of the important concepts of OOP, such as objects, classes, and inheritance, are supported, and these are mostly enough to solve many problems you wish to model using OOP. In the following section, we will briefly look at classes and how these are related to OOP in JavaScript.
Classes
Classes in OOP act like a blueprint for an object. That is, they define a template of an abstract object in such a way that multiple copies can be made by following that blueprint. Copies here are officially called instances. So, in essence, if we define a class, then we can easily create multiple instances of that class.
In ECMA 2015, the class keyword was introduced to JavaScript, and this greatly simplified the creation of classes in the language. The following code snippet shows how to model a User
object using the ES16 class
keyword:
class User { constructor(firstName, lastName, email) { this.firstName = firstName; this.lastName = lastName; this.email = email; } getFirstName() { return this.firstName; } getLastName() { return this.lastName; } getFullName() { return `${this.firstName} ${this.lastName}`; } getEmail() { return this.email; } setEmail(email) { this.email = email; } } let Person1 = new User("John", "Benjamin", "john@some-email.com") console.log(Person1.getFullName()); console.log(Person1.getEmail()); // outputs // "John Benjamin" // "john@someemail.com"
By using the class
keyword, you can wrap both data (names and email) with functionality (functions/methods) in a cleaner way that aids easy maintenance as well as understanding.
Before we move on, let's break down the class template in more detail for a better understanding.
The first line starts with the class
keyword and is usually followed by a class name. A class name, by convention, is written in camel case, for instance, UserModel
or DatabaseModel
.
An optional constructor can be added inside a class
definition. A constructor
class is an initialization function that runs every time a new instance is created from a class. Here, you'll normally add code that initializes each instance with specific properties. For instance, in the following code snippet, we create two instances from the User
class, and initialize them with specific properties:
let Person2 = new User("John", "Benjamin", "john@some-email.com") let Person3 = new User("Hannah", "Joe", "hannah@some-email.com") console.log(Person2.getFullName()); console.log(Person3.getFullName()); //outputs // "John Benjamin" // "Hannah Montanna"
The next important part of a class is the addition of functions. Functions act as class
methods and generally add a specific behavior to the class. Functions are also available to every instance created from the class. In our User
class, methods such as getFirstName
, getLastName
, getEmail
, and setEmail
are added to perform different functions based on their implementation. To call functions on class instances, you typically use a dot notation, as you would when accessing an object's property. For example, in the following code, we return the full name of the Person1
instance:
Person1.getFullName()
With classes out of the way, we now move to the next concept in OOP, called inheritance.
Inheritance
Inheritance in OOP is the ability of one class to use properties/methods of another class. It is an easy way of extending the characteristics of one class (subclass/child class) using another class (superclass/parent class). In that way, the child class inherits all the characteristics of the parent class and can either extend or change these properties. Let's use an example to better understand this concept.
In our application, let's assume we already have the User
class defined in the previous section, but we want to create a new set of users called Teachers
. Teachers are also a class of users, and they will also require basic properties, such as the name and email that the User
class already has. So, instead of creating a new class with these existing properties and methods, we can simply extend it, as shown in the following code snippet:
class Teacher extends User { }
Note that we use the extends
keyword. This keyword simply makes all the properties in the parent class (User
) available to the child class (Teacher
). With just the basic setup, the Teacher
class automatically has access to all the properties and methods of the User
class. For instance, we can instantiate and create a new Teacher
in the same way we created a User
value:
let teacher1 = new Teacher("John", "Benjamin", "john@someemail.com") console.log(teacher1.getFullName()); //outputs // "John Benjamin"
After extending a class, we basically want to add new features. We can do this by simply adding new functions or properties inside the child class template, as shown in the following code:
class Teacher extends User { getUserType(){ return "Teacher" } }
In the preceding code snippet, we added a new method, getUserType
, which returns a string of the user
type. In this way, we can add more features that were not originally in the parent
class.
It is worth mentioning that you can replace parent functions in the child
class by creating a new function in the child
class with the same name. This process is called method overriding. For instance, to override the getFullName
function in the Teacher
class, we can do the following:
class User { constructor(firstName, lastName, email) { this.firstName = firstName; this.lastName = lastName; this.email = email; } getFirstName() { return this.firstName; } getLastName() { return this.lastName; } getFullName() { return `${this.firstName} ${this.lastName}`; } getEmail() { return this.email; } setEmail(email) { this.email = email; } } class Teacher extends User { getFullName(){ return `Teacher: ${this.firstName} ${this.lastName}`; } getUserType(){ return "Teacher" } } let teacher1 = new Teacher("John", "Benjamin", "john@someemail.com") console.log(teacher1.getFullName()); //output // "Teacher: John Benjamin"
A question may arise here: What if we want to initialize the Teacher
class with additional instances besides firstname
, lastname
, and email
? This is achievable, and we can easily extend the constructor function by using a new keyword, super
. We demonstrate how to do this in the following code:
// class User{ // previous User class goes here // ... // } class Teacher extends User { constructor(firstName, lastName, email, userType, subject) { super(firstName, lastName, email) //calls parent class constructor this.userType = userType this.subject = subject } getFullName() { return `Teacher: ${this.firstName} ${this.lastName}`; } getUserType() { return "Teacher" } } let teacher1 = new Teacher("Johnny", "Benjamin", "john@someemail.com", "Teacher", "Mathematics") console.log(teacher1.getFullName()); console.log(teacher1.userType); console.log(teacher1.subject); //outputs // "Teacher: Johnny Benjamin" // "Teacher" // "Mathematics"
In the preceding code, we are performing two new things. First, we add two new instance properties (userType
and subject
) to the Teacher
class, and then we are calling the super
function. The super
function simply calls the parent class (User
), and performs the instantiation, and immediately after, we initialize the new properties of the Teacher
class.
In this way, we are able to first initialize the parent properties before initializing the class properties.
Classes are very useful in OOP and the class
keyword provided in JavaScript makes working with OOP easy. It is worth mentioning that under the hood, JavaScript converts the classes template to an object, as it does not have first-class support for classes. This is because JavaScript, by default, is a prototype-based, object-oriented language. Hence, the class interface provided is called syntactic sugar over the underlying prototype-based model, which JavaScript calls under the hood. You can read more about this at the following link: http://es6-features.org/#ClassDefinition.
Now that we have a basic understanding of OOP in JavaScript, we are ready to create complex applications that can be easily maintained. In the next section, we will discuss another important aspect of JavaScript development, which is setting up a development environment with modern JavaScript support.