The easy approach to learning Object-Oriented Programming in JavaScript
Tags: javascript, oop, webdev, beginners
Published: 2023-03-21 20:58:00

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:

```javascript
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")
 }
}
```

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:

```javascript
class MyClass {
  constructor() { ... }
  // class methods
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
```

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

```javascript
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}`)
}
}
```

The above defines a `User`class 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 `User`class:

```javascript
class User {
 ...
  // constructor function 
    constructor(firstName, lastName, dob, branch){
     this.firstName = firstName;
     this.lastName = lastName;
     this.dob = dob;
     this.branch = branch;
    } 
 ...
}
```

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 `User`object.

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:

```javascript
new constructor(arg1, arg2,...)
```

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

```javascript
//creating a new object from User class
const cus1 = new User (arg1, arg2,...)
```

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 `User`class in our app.

```javascript
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)
```

* The above creates three User objects in our app.
    
* These three objects were derived from the defined `User`blueprint. 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bcdl47z60hmect8pg97m.png)
* Each object can have access to the `logIn()` method declared in the class.
    
* See the code below
    

```javascript
//access the methods defined in the User class
firstUser.logIn()
```

```javascript
"Welcome Emmanuel"
```

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:

```javascript
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')
 }
}
```

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:

```javascript
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}`)
};
}
```

* 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:

```javascript
childClassName extends parentClassName{
    //child class definition
}
```

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.

```javascript
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()
```

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ky8dhipee9307a8hlv62.png)

**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:

```javascript
//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
```

## 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:

```javascript
//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')
}
```

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:

```javascript
//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')
  }
 }
}
```

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

```javascript
//access method from the instance of the class
branchManager.promoteManager()
```

### 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

```javascript

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)
```

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:

```javascript
  //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()
```

The output of the code will be :

```javascript
Uncaught TypeError: manager.promoteManager is not a function
```

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.***