The easy approach to learning Object-Oriented Programming in JavaScript

efkumah

Emmanuel Fordjour Kumah

Posted on March 21, 2023

The easy approach to learning Object-Oriented Programming in JavaScript

The core idea in object-oriented programming is to divide a program into smaller pieces and make each piece responsible for managing its own data.

This article provides a comprehensive but easily-to-understand approach to learning OOP. The objective is to understand the basic concepts of class-based object-oriented programming

At the end of the article, you will learn the following concepts:

  • Objects

  • Classes

  • Abstraction

  • Inheritance

  • Polymorphism

  • Encapsulation

Let's get started!

What is Object-Oriented Programming?

Object-oriented programming (OOP) is a programming methodology where all software units are organized around objects rather than functions and logic.

Like how cells form the basic unit of life, objects form the basic units of object-oriented programming.

OOP provides a way of creating and managing different aspects of your app in isolation from objects and connecting them independently. It also facilitates code reusability, provides cleaner maintainable code, and removes redundancy.

OOP consists of four pillars

  • Encapsulation

  • Abstraction

  • Inheritance

  • Polymorphism

These concepts will be detailed in our next sections, in the meantime, let's explore the importance of OOP

Importance of OOP

Object Oriented Program was introduced to remove the deficiency encountered in Procedural Programming.

The procedural style of programming uses a sequential approach and regards data and procedures (functions) as two different entities. Whenever you write your application using Procedural Programming, you will find that functions are scattered throughout the program as it develops.

The disadvantages of the Procedural approach are as below:

  • The Procedural code is often not reusable as you may have to copy and paste lines of code (recreate the code) if it is required in an aspect of the application.

  • Because priority is given to the procedures rather than the data, data is exposed to the entire application posing issues in some data-sensitive cases.

With the introduction of OOP, data and functionality are combined into a single entity called an object.

  • This makes it easier to create and manage different aspects of your app in isolation and connect them independently.

  • Because our application is built in units (objects) our code is well organized, more flexible, and easier to maintain ( avoiding "spaghetti code").

Understanding Objects in OOP

Objects are the core of Object-oriented programming. An object can represent a real-life entity with some data and behavior. It is a collection of related data (properties) and or functionality(methods).

Consider a common real-life object, a dog. The dog has certain characteristics: color, name, weight, etc. It can also perform some actions: bark, sleep, eat, etc.

In OOP terms, these characteristics of the dog are referred to as Properties , and the actions are referred to as methods.

In building a web app, each unit will be represented by an object.

Consider a scenario where you are developing a digital banking web app. An example of a real-world entity of the app will be a Person.

The Person represents an object consisting of properties and methods.

Examples of these properties could be first name, last name, date of birth, age, account details, and branch.

The Person can have methods such as: logging in, changing the account details, checking the account balance, withdrawing and depositing funds, etc.

Below is a representation of the Person object:

const Person = { 
  //properties
  firstName: "Emmanuel",
  lastName: "Kumah",
  dob: "22/12/99",
  branch: "Accra"

  //methods
 logIn(){
  console.log(`Welcome ${this.firstName}`)
}
  changeAccDetails(){
  console.log('Account details changed')
}
  checkAccBalance(){
   console.log('Checking balance...')
 }
 withdrawFunds(){
   console.log("Cash withdrawn")
 }
 depositFunds(){
   console.log("Cash deposited")
 }
}
Enter fullscreen mode Exit fullscreen mode

Whenever Objects are utilized, other parts of the app do not need to worry about what's going on inside an object. Because it provides an interface to other code that wants to use it, but it maintains its own private, internal state.

With our digital bank app, assuming we have over 25K persons on the app, we may have to declare 25K objects each with their unique properties and methods.

To solve this tedious task of creating 25K unique objects, JS introduces a concept called Classes

Classes and instances

Classes provide a way of grouping similar objects under one umbrella.

For instance, the over 25K persons on the app, can be categorized into users

Because each user represents an object and s*hares common properties and methods, we generally create a conceptual definition representing the **types of objects we can have in the application.*

Whenever we are dealing with a lot of objects in our app, we can avoid this repetition of writing properties and methods for each object and speed up development by defining a template or blueprint (which is a high-level plan) for creating these objects.

This template is referred to as a Class

What is a class?

Classes specify a layout to create objects with predefined properties and methods.

It can be likened to an Architect who has designed a blueprint for creating houses. Based on the blueprint, we can develop as many buildings as possible, each sharing common features defined in the blueprint.

The class lists the data and methods that will be included whenever a new object is created.

The basic Class syntax is as below:

class MyClass {
  constructor() { ... }
  // class methods
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now, instead of defining each Person object resulting in over 25K objects, we use a class to design a layout to help us easily create these objects.

We will give our class the name User since each Person is a user of the app, and we will define all the properties and methods common to each user

Below is how the User class is defined

class User{
   // initialize properties
  firstName,
  lastName,
  dob,
  branch,
  // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    }  
   //action performed by all users
 logIn(){
  console.log(`Welcome ${this.firstName}`)
}
}
Enter fullscreen mode Exit fullscreen mode

The above defines a Userclass with

  • Four properites: first name, last name, dob, branch

  • The constructor function: useful for creating the objects

  • The login() methods available to all users.

Instances

A Class does nothing on its own, it serves as a guide for creating specific objects. Each object we create becomes an instance of the class.

An instance is an object containing data and behavior defined by the class.

Constructor

In creating an instance we use a special method in the body of the class called a constructor.

Typically, the constructor is written out as part of the class definition. It enables assigning values to the properties we want to specify in the new instance.

Below is the snippet of the constructor in the Userclass:

class User {
 ...
  // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    } 
 ...
}
Enter fullscreen mode Exit fullscreen mode

The constructor in the code above takes four parameters, so we can assign values to the firstName, lastName, dob and branch properties when we create a new Userobject.

In the next section, we will learn how to create objects from a class.

Creating new Objects from a Class

We use the new operator to create a new object (instance of the class) with all the listed methods and properties.

The syntax is as below:

new constructor(arg1, arg2,...)
Enter fullscreen mode Exit fullscreen mode

Let's create an instance of the Userclass in our earlier example using the new operator.

//creating a new object from User class
const cus1 = new User (arg1, arg2,...)
Enter fullscreen mode Exit fullscreen mode

When new User(arg1, ag2, ...) is called:

  • A new object is created.

  • The constructor function (which has the same name as the Class) runs with the given arguments so it can assign the values to the new object

Below is how we create three different objects from the Userclass in our app.

class User{
   // initialize properties
  firstName
  lastName
  dob
  branch
  // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    }  
   //actions performed by the user
 logIn(){
  console.log(`Welcome ${this.firstName}`)
};
}

//create a user object from User class
const firstUser = new User('Emmanuel','Kumah','22/12/89','Accra')
const secondUser  = new User('Robert','Taylor','12/12/77', "Accra")
const thirdUser = new User('Edmond',"Sims",'8/03/99', 'Accra')

//log the details 
console.log(firstUser);
console.log(secondUser);
console.log(thirdUser)
Enter fullscreen mode Exit fullscreen mode
  • The above creates three User objects in our app.

  • These three objects were derived from the defined Userblueprint. Because we used the User blueprint, we did not need to write the entire code (both properties and methods) for each user we created.

The output of the code above is as below:

OOP

  • Each object can have access to the logIn() method declared in the class.

  • See the code below

//access the methods defined in the User class
firstUser.logIn()
Enter fullscreen mode Exit fullscreen mode
"Welcome Emmanuel"
Enter fullscreen mode Exit fullscreen mode

In the next section, we explore Inheritance, one core pillar of OOP

Inheritance

Our digital banking app will definitely need clients who can log in and perform transactions ( deposit and withdraw cash).

We also need Managers who will be responsible for monitoring the activities of the bank.

The Client and BranchManager will have these properties and methods in common:

  • Properties: first name, last name, date of birth, and branch

  • Method: logIn()

Besides logging in to the app, the branch manager can view the details of all users and their transaction history. The client can also deposit and withdraw cash

Let's represent the BranchManager and Client as below:

class BranchManager {
 // initialize properties
  firstName
  lastName
  dob
  branch
  // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    }  
   //actions performed by the branch manager
 logIn(){
  console.log(`Welcome ${this.firstName}`)
};
 //additional actions
  viewTransactions(){
   console.log(`${this.firstName} can view transactions `)
  }
  viewUserDetails(){
   console.log(`${this.firstName} can view user details`)
  }

}

// Client class
class Client {
 // initialize properties
  firstName
  lastName
  dob
  branch
  // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    }  
   //actions performed by the branch manager
 logIn(){
  console.log(`Welcome ${this.firstName}`)
};
 depositCash(){
 console.log('Cash deposited')
 }
 withdrawCash(){
  console.log('Cash withdrawn')
 }
}
Enter fullscreen mode Exit fullscreen mode

We notice that the User class we defined earlier and the Branch Manager and Client classes above share certain properties and methods in common.

  • They both have the first name, last name, branch, and date of birth

  • They both have the logIn() method

Because they share certain properties and methods in common, it is generally recommended we define a class User, that possesses all the common properties and methods for all other entities we will need in our app. This helps reduce redundancy

The class User is defined as below:

class User {
    // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    }  
   //common method to all entities
 logIn(){
  console.log(`Welcome ${this.firstName}`)
};
}
Enter fullscreen mode Exit fullscreen mode
  • The class User defined above is referred to as the superclass or parent class

  • Whenever we declare a parent class, we can declare child classes that will acquire all the properties and methods of the parent. This phenomenon is referred to as inheritance

Inheritance is the ability of a class to derive properties and characteristics from another class while having its own properties as well.

In real-life, inheritance is how children are able to acquire certain features like height, the color of hair, the shape of the nose, intelligence, etc, from their parents.

Similarly, in programming, the child classes can acquire all the properties and methods of the parent.

The goal of inheritance is to reuse common logic

Inherit using the extend and super keywords

The keyword extend is used to show inheritance between classes and to declare the parent class we want to succeed from.

The syntax is as below:

childClassName extends parentClassName{
    //child class definition
}
Enter fullscreen mode Exit fullscreen mode

Using the keyword extends, we can instruct the BranchManager and Client classes to inherit all the properties and methods of the parent class User. The BranchManager and Client classes can add additional properties and methods to their class.

In the code below, we extend the User class to include the BranchManager and Client classes.

class User {
    // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    }  
 logIn(){
  console.log(`Welcome ${this.firstName}`)
};
}
//Client class inherits from User
class Client extends User {
 constructor(firstName, lastName, dob, branch, accType,amtDeposited){
      //call the super before adding option
       super(firstName, lastName, dob, branch);
       //added new properties 
       this.accType = accType;
       this.amtDeposited = amtDeposited 
    }
 //additional methods 
 depositCash(){
  console.log(`${this.firstName} deposited ${this.amtDeposited} USD`)
 }
 withdrawCash(){
  console.log(`${this.firstName} withdrew cash`)
 }
}

//BranchManager class inherits from User
class BranchManager extends User {
    constructor(firstName, lastName, dob, branch, expLevel){
      //call the super before adding option
       super(firstName, lastName, dob, branch);
       this.expLevl = expLevel;//added new props
    }
    //additional actions
  viewTransactions(){
   console.log(`${this.firstName} can view transactions `)
  }
  viewUserDetails(){
   console.log(`${this.firstName} can view user details`)
  }
}
//create two instances of the Client
const client1 = new Client('Ruby', 'Smit', '05/09/88', 'Accra', 'Savings', 20) 
console.log(client1)

//access all methods of the Client class
 client1.logIn()
 client1.depositCash()
 client1.withdrawCash()

// create an instance of Branch manager
const firstManager = new BranchManager('Jose','Adams','22/09/1999',"Kumasi",12)

console.log(firstManager)
//access the login method
firstManager.logIn()
//access the additional method
firstManager.viewUserDetails()
firstManager.viewTransactions()
Enter fullscreen mode Exit fullscreen mode
  • The parent class is the User whiles the the BranchManager and Client are the child classes.

  • Using the keyword extend , the BranchManager and Client inherits all the properties and methods of the User class.

  • The super is used to call the constructor of its parent class to access the parent's properties and methods.

  • The this.expLevl = expLevel , this.accType = accType and this.amtDeposited = amtDeposited allows us to add extra properties to the BranchManager and Client classes respectively.

  • We also added two extra methods viewTransactions and viewUserDetails to BranchManager

  • In the Cleint class, we added the depositCash and withdrawCash methods.

  • Because the BranchManager and Client classes inherit from the User class, the logIn() has been added automatically, and can be accessed by instances of the class.

The concept of inheritance now allows any instance of the BranchManager and Client classes to inherit all properties and methods defined in the User class.

Even though the logIn() was not defined in the BranchManager and Client classes, we are still able to access it.

The output of the code above is as below:

Javascript oop

Importance of inheritance:

  • Inheritance enables us to define a class that takes all the functionality from a parent class and allows you to add more

  • It helps in code reusability, provides cleaner maintainable code, and removes redundancy

Polymorphism

Polymorphism allows a method with the same name to have a different implementation in different classes.

In Polymorphism, a child class can overwrite a method that is inherited from a parent class. The goal is to permit greater flexibility and make code reusable.

For instance, the logIn() method in the User can be overwritten in the BranchManager class.

See the code below:

//BranchManager class inherits from User
class BranchManager extends User {
//same code snippet used earlier
 ...
 //overwrite method in superclass
 logIn(){
  console.log(`Howdy ${this.firstName}`)
   }
}

const manager = new BranchManager('Simon','Tagoe','13/02/2000',"Accra",14)
//access the logIn() method
manager.logIn()

// output will be 
Howdy Simon
Enter fullscreen mode Exit fullscreen mode

Encapsulation

Encapsulation is the process of wrapping data and methods that work with them in a single component ( a Class). The concept of encapsulation is to allow only the object access to the state data. It is used to hide the values or state of the objects inside the class, to prevent modification by unauthorized code.

Its significance is to keep the data private to the object and avoid any direct access outside the object

Encapsulation encourages information hiding and reduces complexity by removing implementation details

Imagine using a TV remote control, you have some interface to work with such as the power button, and numbers button. You can use these interfaces for some actions, such as turning the TV on or off, changing the current TV channel, saving a favorite channel, etc.

You use these interfaces without bothering about how it works. In other words, the actual implementation of the interface is hidden from you.

Similarly, in OOP, you can use an object by callings its methods. Whether you wrote these objects yourself or utilized a third-party library, your code pays no attention to how the methods work internally.

The data contained in an object can only be accessed through a public interface (that is the object's own method). Whenever you want to use data contained in an object, you define a method within that object to handle the action.

It is considered bad practice to retrieve data inside an object and write separate code to perform actions with the data outside of the object.

For instance, if you want to promote a Branch Manager with more than 5years of experience to the next rank, we might have to implement it as below:

//create a instance of Branch Manager class
const branchManager = new BranchManager('firstName','lastName', '04/1/80', 7)
// code snippet to promote a manager
if(branchManager.expLevl >= 5){
 console.log('You will be promoted to the next rank')
}else {
 console.log('better luck next time')
}
Enter fullscreen mode Exit fullscreen mode

The code to promote a manager can be used at any required place in our system.

With this implementation, if we decide to change the criteria for promoting a manager, we will have to find everywhere the code is implemented in our app and update it.

However, the idea of encapsulation requires that data inside an object should only be accessed by the object's own method.

It would be better to have a promoteManager() method within the BranchManager class that handles the logic in just one place. So that when the logic is being updated, it is updated in only one place.

Below is the recommended procedure:

//BranchManager class inherits from User
class BranchManager extends User {
    constructor(firstName, lastName, dob, branch, expLevel){
      //call the super before adding option
       super(firstName, lastName, dob, branch);
       this.expLevl = expLevel;//added new props
    }
    //additional actions
  viewTransactions(){
   console.log(`${this.firstName} can view transactions `)
  }
  viewUserDetails(){
   console.log(`${this.firstName} can view user details`)
  }
  //method to promote manager
  promoteManager(){
 if(this.expLevl >= 5){
 console.log('You will be promoted to the next rank')
  }else {
 console.log('better luck next time')
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

We can now access the promoteManager() from the instance of the class

//access method from the instance of the class
branchManager.promoteManager()
Enter fullscreen mode Exit fullscreen mode

Encapsulation: Private properties and method

An object's internal data can be kept private, making it accessible only by the object's own method and not from other objects.

For instance, if we don't make the expLevel property private, someone can access it and change its value to this.expLevel = 10

To make the property private, we place an underscore ( _ ) next to the property name. For instance (this._propertyName)

Let's make the expLevl private in the BranchManager class


class User {
 ...
}
//BranchManager class inherits from User
class BranchManager extends User {
    constructor(firstName, lastName, dob, branch, expLevel){
      //call the super before adding option
       super(firstName, lastName, dob, branch);
      //protected property
       this._expLevl = expLevel;
    }
   ...
  //method to promote manager
  promoteManager(){
 if(this._expLevl >= 5){
 console.log('You will be promoted to the next rank')
  }else {
 console.log('better luck next time')
  }

}

// create an instance of Branch manager
const firstManager = new BranchManager('Jose','Adams','22/09/1999',"Kumasi",12)
// try accessing the expLevl property and the output wil be undefined
console.log(firstManager.expLevl)
Enter fullscreen mode Exit fullscreen mode

This implementation does not make the property truly private hence it's termed protected property.

To make the property truly private, we prepend the property with a # and it should be defined outside any method. Eg. #firstName

We can also protect a method to prevent it from being accessed outside of the class.

Below is how to protect the promoteManager method:

  //protect the promoteManager method
  _promoteManager(){
 if(this._expLevl >= 5){
 console.log('You will be promoted to the next rank')
  }else {
 console.log('better luck next time')
  }
 }
//try accessing the method 
manager.promoteManager()
Enter fullscreen mode Exit fullscreen mode

The output of the code will be :

Uncaught TypeError: manager.promoteManager is not a function
Enter fullscreen mode Exit fullscreen mode

The benefits of encapsulation are outlined below:

  • Data within an object, cannot be modified unexpectedly by an external code in an entirely different part of the application

  • Whenever we use a method, we only need to know the result and don't care about the internal implementation.

  • Functionality is defined in a logical place: that is the place the data is kept hence it becomes easy to alter the functionality of your application.

Abstraction

Abstraction is hiding or ignoring details that don't matter, allowing us to get an overview perspective of the thing we are implementing.

The objective of abstraction is to handle the complexity by hiding all irrelevant details from the user resulting in a simplified design that is easier to understand and maintain.

It is usually achieved by specifying methods for interacting with objects through interfaces. The implementation details of these methods are hidden behind these interfaces

This allows programmers to work with objects at a higher level of abstraction, without having to worry about the low-level details of their implementation.

For instance, whenever you purchase a smartphone, you may be only interested in the following

  • How to switch the phone on or off

  • How to dial a number to make a call

  • How to access information via the internet

The low-level details like

  • How does the phone gets connected to the internet to pull the specified information

  • What happens under the hood when you switch the phone on or off

  • How the gyroscope sensor works to enable you to rotate the phone

  • How the vibration, ambient temperature, magnetic field sensors, etc work are irrelevant to you hence these details can be hidden making the phone easy to use

Abstraction deals with simplification. For instance the Array object in JavaScript enables storing a collection of multiple items under a single variable name.

It has methods like:

  • Array.length that enables you to get the number of items in the array

  • Array.push() that pushes an element into the array etc

Whenever you use these methods, you don't need to know about all the intricacy that went into defining the Array object. Those low-level details have been abstracted (removed) to make it easy to use the method. The goal is to simply use the methods expose to you to complete specific tasks.

One main benefit of abstraction is that it reduces the impact of change. The developers of the Array object can change the internal implementation, for instance, to improve its performance. However, as long as there is still array.push() and array.length methods, you can continue to use the Array methods just like before without being negatively impacted by the changes

Summary

  • The core idea in object-oriented programming is to divide a program into smaller pieces and make each piece responsible for managing its own state.

  • Classes specify a layout to create objects with predefined properties and methods.

  • Inheritance is the ability of a class to derive properties and characteristics from another class while having its own properties as well.

  • Polymorphism allows a method with the same name to have a different implementation in different classes.

  • Encapsulation is the process of wrapping data and methods that work with them in a single component ( a Class).

  • Abstraction is hiding or ignoring details that don't matter, allowing us to get an overview perspective of the thing we are implementing.

If you have found value in this article, or seen areas of improvement, please do comment. Also kindly share the article with your social networks, it might be beneficial to someone else.

💖 💪 🙅 🚩
efkumah
Emmanuel Fordjour Kumah

Posted on March 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related