Object Oriented Programming in JavaScript

·

8 min read

The following blog covers the OOP concepts such as classes, inheritance, prototype inheritance

Class vs Instance

Class is a factory which can produce instance. Consider 'class' is a car factory and maruti suzuki is an instance produced. Now every car produced will be unique.

So every instance is also unique. Class promote re-usability.

Declaring a class

We start by writing the keyword 'class' followed by the custom name of the class. It must be in UpperCamelCase.

class CustomClassName{
}

Constructor

When we call the instance of the class, constructor function will be automatically called.

class Person{
    constructor(){
        console.log("This will be called automatically");
    }
}

Similarly we can pass arguments while creating instance of the class and we need to add variable parameters to handle the arguments passed.

class Person{
    constructor(firstName, lastName){
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

const person = new Person("Siddhant", "Raj");

Instance variables

These are the variables that belong to a specific instance of a class. The firstName and lastName are captured using the argumetns of the instance parameter. But we can also set an instance variable without capturing the argumetns from constructor.

class Person{
    constructor(firstName, lastName){
        this.firstName = firstName;
        this.secondName = secondName;
        // set an instance variable
        this.age = 25;
    }
}
const person = new Person("Sam", "Wilson");

Instance Methods

Classes group common functionality together. So we declare some functions inside the class to perform custom tasks. We will for now use this.variable_name.


class Person{
    constructor(name){
        this.name = name;
    }

    getName(){
        return `The name is ${this.name}`
    }
}

const person = new Person("Siddhant");
person.getName() // "This name is Siddhant"

See how we don't have to write 'function' in front of getName(). We cannot call Person.getFullName() becasue for that getName() has to be static method.

Q: Create a class containing taking arguments as firstName, age. Create two methods that will return name and whether the age is greater than 18 or not Ans:


class User{
    constructor(firstName, age){
        this.firstName = firstName;
        this.age = age;
    }

    getName(){
        return `The name is ${this.name}`
    }

    ageGreaterThanEighteen = () => {
        return this.age > 18 ? true : false;
    }

}
const user = new User("Siddhant", 25);
user.getName(); // "The name is Siddhant"
user.ageGreaterThanEighteen(); // true

}

Whatever we are doing, we doing object oriented programming. OOP means we are describing the real world with classes and use them as instances. OOP has features like getter, setter, static methods, inheritance and more.

Calling an instance method from another instance method

Just like we can call instance variable using this.variableName, we can call one method from another method using this.methodName()

class User{
    constructor(firstName, age){
        this.firstName = firstName;
        this.age = age;
    }

    getName(){
        return `The name is ${this.name}`
    }

    ageGreaterThanEighteen = () => {
        return this.age > 18 ? true : false;
    }
    ------------------------
    canVote = () => {
        return this.ageGreaterThanEighteen()
    }
    -------------------------
}

See how we use this.ageGreaterThanEighteen() for the canVote() instance method. We can repeat the same syntax whereever we need to call an instance method within the class.

Setter and agetter methods

Remember one thing, whenever we call the variable using the set and get methods, we need to use _ 'underscore' in front of the instance variable else we will be stuck in an infinite loop.

The _ 'underscore' tells the JavaScript that the variable is an internal variable and cannot be accessed from outside.

This is how we do a setter and getter in JavaScript


class User {
    constructor(age) {
        this.age = age; // calls set age(value)
    }

    get age() {
        console.log("age getter");
        return this._age;
    }

    set age(value) {
        console.log("age setter");
        this._age = Number.parseInt(value, 10);
    }
}

See how we are not defining a method 'age()'. The setter and getter are doing that for us. When creating getters and setters, make sure to prefix the new instance variable with an _ to prevent creating an infinite loop.

Q: Create a class with setter and getter containing an instance variable called cents that reflects the cent value from the 'amount' it receives. One USD = 100 cents. If we are creating an instance of class and passing 1 as argument, or if we set the 'amount' to be 1 we should get 100 as output.

Ans: Since we have to set amount and cents where amount is the argument we pass and cents (_cents) is the internal instance variable and we have to access the .amount to get the _cents value we will create a setter and getter for amount. The constructor will also take 'amount' as argument and we will initialize the instance varaible as 'amount'

class Payment{
    contructor(amount){
        this.amount = amount;
    }

    set amount(value){
    // see how we didn't set a function amount() but a setter and getter
        this._cents = Number.parseInt(value,10) * 100;
    }

    get amount(){
        return this._cents;
    }
}

Now we have created the class Payment we can set the amount and will get the value back in cents. This is how we will do it

const payment = new Payment(10);
payment.amount // 1000
payment.amount = 8; // we are setting the amount in USD
payment.amount // 800

You will have to do console.log() to payment.amount

Static methods in JavaScript

We saw how we are able to call the instance methods only after creating an instance of the class. But we can directly access the methods inside the class without creating the instance if the method are static.

We have used it earlier like Number.parseInt() or Math.ceil(). We don't need to create an instance of Number or Math.

class Country{
    static getCountry(){
        return "India"
    }
}

When to use static method?

  1. When the output will be same despite changing the argument.

  2. If we don't need to use this . If we have to use 'this' inside the static method, the method should not be static.

Method chaining

How many times have we used something like this in programming?

const fetch_data = fetch(url).
        then(() => console.log('Here')).
        catch(err => console.log(err)).
        finally(() => console.log('end');

We are chaining a set of methods one after another because every method is returning something. This putting sequence of methods is called as method chaining. How we are gonna do this in our custom class? We need to return this . If we are not returning this we cannot access the method chaining.

Consider the following example :


const course = new Course("JavaScript");
course.markAsCompleted(true).setGrade(18).requestCertification();

For above method chaining to work we need to have the instance methods markAsCompleted, setGrade and requestCertifcation returning 'this' . See the below example


class Course{
    constructor(name){
        this.name = name;
        this.isCompleted = false;
    }

    markAsCompleted(isCompleted){
        this.isCompleted = isCompleted;
        return this;
    }

    setGrade(grade){
        this.grade = Number.parseInt(grade,10);
        return this;
    }

    requestCertification(){
        this.askedForCertification = true;
        return this;
    }
}

Now what exactly will we get if we console.log()

course.markAsCompleted(true).setGrade(18).requestCertification();
  ```  ??

We will get an object which will contain all the instance variables along with their values

console.log(course.markAsCompleted(true).setGrade(18).requestCertification()) ; // { name : "JavaScript", isCompleted: true, grade :18, askedForCertification:true }

Go ahead an try seeing it yourself. See how we declared grade and askedForCertification later but still we are getting it in our response when we applied method chaining.

So we don't really need to worry about declaring instance variable in the constructor itself, we can do it later in our instance methods as per requeirement and the class will still work.

Inheritence

To avoid writing same code again and again we declared classes but what if we have to declare multiple classes which has common methods?

Consider we made a class describing the mobile phones. Now we have functionalitites like camera, RAM, battery same for iPhone, Android and Windows phone but we also have different features like 5G connectivity, gorilla glass, warranty, power consumed. So instead of declaring the classes again and again, we can inherit the methods from a parent class and override some methods for the different types of mobiles.

class Mobile{
    constructor(name, price){
        this.name = name;
        this.price = price;
    }

    getName(){
        return this.name;
    }

    getPrice(){
        return this.price;
    }
}

class iPhone extends Mobile{
    getFeatures(){
        return "Waterproof, Round Edge, Metal Body";
    }
}

class Android extends Mobile{
    getName(){
        return "Android"
    }
}

This is for example. We are doing method over-riding in Android and declaring getFeatures() in iPhone.

But what will happen if we want to declare the android system too! The main Mobile class has a constructor which is taking the name and price but what if we want to declare an additional instance variable in the Android class?

For that we have the super() keyword. super() takes arguments as the name of the variables of the parent class and then we declare the new instance variable.

class  Android extends Mobile{
    construtor(name ,price, androidVersion){
        super(name, price);
        this.androidVersion = androidVersion;
    }

    getAndroidVersion(){
        return this.androidVersion;
    }
}

super(name, price) is calling the parent class constructor.

We can also call the parent class instance method using super.methodName(). This is how we can call the instance method

class  Android extends Mobile{
    construtor(name ,price, androidVersion){
        super(name, price);
        this.androidVersion = androidVersion;
    }

    getAndroidVersion(){
        return this.androidVersion;
    }
----------------------- // calling the instance methods from parent class ---
    getAllDetails(){    
        return super.getName() + " " + super.getPrice();
    }
-------------------------
}

So when we call super() we are invoking the constructor of the parent class.

Real classes in JavaScript?

We declare class and what if we put the class declaration inside the console.log(typeof(custom_class)). We will see that custom_class is a function for JavaScript.

The class keyword is a syntactic sugar coating to make the code readable.

prototype

Before the class keyword, this is how we declared classes

function Rectangle(widht, height) {
    this.width = width;
    this.height = height;
}
------------------- BELOW IS HOW WE DECLARE USING class KEYWORD ----------
class Rectangle{
    constructor(width, height){
        this.width = width;
        this.height = height;
    }
}

We declare function Rectangle but how do we add the instance methods now?

We use the keyword 'prototype' to declare the instance methods like we were doing it previously.

Reactangle.prototype.isSquare() = function(){
    return this.wisdth === this.height
}

'prototype' is an object that contains all the instance methods that a certain function can have. We are declaring a new method .isSquare() and adding it to the prototype.

Everytime we are creating a new instance of Rectangle we will have an instance method .isSquare()

Often we see String.prototype.trim() ; .trim() is a method which can be called on any new instance of string that we create.

Thanks for reading patiently. There is a concept of private and public methods and variables which I will be covering in a separate blog.