JS Object Oriented Programming

26 Mar 2023  Amiya pattanaik  15 mins read.

Object Oriented programming (OOP) is a programming concepts that involves organizing code into objects that can interact with each other. JavaScript is a language that also supports OOP. Technically JavaScript is a prototype based programming language. It uses an object as a template to get the properties of other objects. But, JavaScript ES2015 onwards it made life easy like any other class based languages such as JAVA and C++.

JavaScript Objects

An object in JavaScript is a collection of properties, where each property is a key-value pair. The keys are strings that represent the names of the properties, and the values can be of any data type, including other objects.

Lets take an example of an object in JavaScript:

const employee = {
  name: 'John Smith',
  age: 45,
  address: {
    street: '2020 Caravan Street',
    city: 'San Jose',
    state: 'California',
    zip: '12345'
  }
};

Here the employee object has three properties: name, age, and address. The address property is itself an object, with four properties of its own. We can access the properties of an object using either dot notation or bracket notation.

console.log(employee.name); // Output: 'John Smith'
console.log(employee['age']); // Output: 45
console.log(employee.address.city); // Output: 'San Jose'

JavaScript Classes

JavaScript introduced the class keyword in ECMAScript 2015. It makes JavaScript seem like an OOP language. But it is just syntactical sugar over the existing prototyping technique. It continues its prototyping in the background but makes the outer body look like OOP. Let’s see now look at how that’s possible. The following example is a general usage of a class in JavaScript:

class JobPosition {
  constructor(role, salary, applicationLink, isRemote)
  {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
}

so the code above creates a JobPosition class with a constructor that takes four arguments: role, salary, applicationLink, and isRemote properties. Now you can create different jobs using the new keyword:

let job1 = new JobPosition(
  "Software Engineer",
  250000,
  "linkedin.com/careers/VP-Marketing/apply",
  true
);

console.log(job1);
// Output:
JobPosition {
  role: 'Software Engineer',
  salary: 250000,
  applicationLink: 'linkedin.com/careers/VP-Marketing/apply',
  isRemote: true
}

let job2 = new JobPosition(
  "Head of Design",
  175000,
  "apple.com/careers/Engineering-Director",
  false
);
console.log(job2);
// Output:
JobPosition {
  role: 'Head of Design',
  salary: 175000,
  applicationLink: 'apple.com/careers/Engineering-Director',
  isRemote: false
}
// In both cases it print out both jobs details

Class Methods

When creating classes, you can add as many properties as you like. For example, if you have a Vehicle class, aside from basic properties like type, color, brand, and year, you probably also want to have methods like start and stop.

To add methods inside classes, you can add them after the constructor function:

class Vehicle {
  constructor(type, color, brand, year) {
    this.type = type;
    this.color = color;
    this.brand = brand;
    this.year = year;
  }
  start() {
    return "Vroom! Vroom!! Vehicle started";
  }
  stop() {
    return "Vehicle stopped";
  }
}

The code above defines a Vehicle class with type, color, brand, and year properties, as well as start, stop methods. To run methods on the object, you can use the dot notation:

const vehicle1 = new Vehicle("car", "red", "Toyota", 2022);
const vehicle2 = new Vehicle("motorbike", "blue", "Harley Davidson", 2022);
console.log(vehicle1.start() ); // returns 'Vroom! Vroom!! Vehicle started'

The code above creates two types of vehicles using the Vehicle class and runs the class methods on them based on the call to your method.

Inheritance in JavaScript

Inheritance is the process of creating a new class based on an existing class. The new class, known as the subclass, inherits the properties and methods of the existing class, known as the superclass. In JavaScript, we can implement inheritance using the extends keyword. Here’s an example:

class Car extends Vehicle {
  constructor(type, color, brand, year) {
    super(type, color, brand, year);
  }
  // class specific method
  start(){
    return 'This start is specific to Car class';
    }
}

const myCar = new Car("Electric", "Black", "Tesla", 2022);

console.log(myCar);           // Car { type: 'Electric', color: 'Black', brand: 'Tesla', year: 2022 }

console.log(myCar.start());   // This start is specific to Car class

console.log(myCar.stop());    // Vehicle stopped

In this example, we declared a class called Car that extends the Vehicle class. The Car class has a constructor that takes four parameters: type, color, brand, year. The constructor calls the super method, which calls the constructor of the Vehicle class and initialize the type, color, brand, year properties. The Car class also has a start() method that logs a message to the console. This start() is a specific implementation of the Car class and whose scope is limited only to the Car class only. Also Car class extends stop() method from the parent Vehicle class as well.

Encapsulation in JavaScript

In the world of programming, encapsulation is the bundling of data with the methods that operate on that data. So it means hiding the internal details of an object and providing a public interface for interacting with it. This helps to prevent outside code from directly modifying the internal state of an object, which can lead to bugs and other issues. Please bear with me as I am going to introduce this important concept one by one with examples and fine tuning the code.

let employee = {
	id: 1010,
	name: "Akash",
	salary: 25000
}

In this employee object , you can access these attributes from outside the object.

console.log(employee.name); // will print Akash

console.log(employee.salary); // will print 2500

You can also change the value in a similar fashion.

let employee = {
	id: 1010,
	name: "Akash",
	salary: 25000
}
employee.salary = 50000;
console.log(employee);

The above code prints { id: 1010, name: ‘Akash’, salary: 50000 } to the console. Here we have modified salary attribute that’s why we see the modified value of 50000. lets try to run the following code.

let employee = {
	id: 1010,
	name: "Akash",
	salary: 25000
}
employee.salary = 'Seventy thousands';
console.log(employee);
We get { id: 1010, name: 'Akash', salary: 'Seventy thousands' } on the console. We don’t want salary to store a string value.

How can we control the access to the attributes of this object?

Encapsulation may be the answer here. With Encapsulation, we can ensure that we give direct access to data to the associated methods in the object and further give access to these methods to the outer world. lets see in this example.

let employee = {
	id: 1010,
	name: "Akash",
	salary: 25000,
  setSalary: function(newSalary){
        if(isNaN(newSalary)){
            throw new Error(`${newSalary} is not a number`)
        }
        salary = newSalary
    }
}
employee.setSalary('Seventy thousands')

// Error: Seventy thousands is not a number
// at Object.setSalary (/test.js:7:19)
// at Object.<anonymous> (/test.js:12:10)

Does this mean that the outer world has no direct access to internal data? No, it does not.

let employee = {
	id: 1010,
	name: "Akash",
	salary: 25000,
  setSalary: function(newSalary){
        if(isNaN(newSalary)){
            throw new Error(`${newSalary} is not a number`)
        }
        salary = newSalary
    }
}
employee.salary = 'Seventy thousands';
console.log(employee);
// Output

// {
//   id: 1010,
//   name: 'Akash',
//   salary: 'Seventy thousands',
//   setSalary: [Function: setSalary]
// }

If you carefully observe the above code, We were able to write a function that puts a check while setting the value for salary, but how do we ensure that the outer world has no direct access to data?

Different ways to achieve Encapsulation in Javascript

1. Using the Functional Scope (partial encapsulation)

As you know a variable inside a functional scope can not be accessed from outside, so using functional scope we can restrict the direct access to data using. Let’s see with an example.

function employee() {
	const id = 1010
	const firstName ='John'
	const lastName = 'Smith'

	function employeeFullName(){
		const employeeFullName = `${firstName}  ${lastName}`
	}
}

In this example, we created a function employee and adding data like (id, firstName, lastName) and a method (employeeFullName) inside it. When this function is used in place of an object, we will be able to restrict the direct access to those members in it. So these variables id, firstName, lastName are not accessible from the outer scope and at the same time we will not be able to access the inner methods as well such as employeeFullName(). This looks like a half baked solution.

Let us now explore how can we achieve Encapsulation by enhancing this concept.

2. Encapsulation Using Closures

what is closure?

A closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time. This means a function is able to remember the variables from the scope it was defined in, even when it now lives outside that scope. Let’s understand with an example

function employee() {
	const id = 1010
	const firstName ='John'
	const lastName = 'Smith'

	function employeeFullName(){
		return `${firstName}  ${lastName}`
	}
	return employeeFullName()
}
console.log(employee()); 	// John  Smith

Here, the inner function employeeFullName() has access to firstName, lastName even when employeeFullName() is called outside the scope of the outer function employee(). When the inner function is returned from the outer function, and by virtue of closure, it is able to access the variables defined in the outer function at the time of the call.

Let’s now see how we can use closures to achieve Encapsulation. The first thing to be achieved here is to restrict access to inner data. Since data inside a function scope is not accessible outside the function, in this example we will initialize the object as a function and declare the variables id, name and salary inside it.

let employee = {
	id: 1010,
	name: "Akash",
	salary: 25000,
}
console.log(employee.salary) //Undefined.

In this above example we restricted the access, we would want to use the methods which operate on the data. lets see how we can do it.

let employee = function(){
	var id = 1010;
	var name = "Akash";
	var salary = 25000;
  function setSalary (newSalary){
        if(isNaN(newSalary)){
            throw new Error(`${newSalary} is not a number`)
        }
        salary = newSalary;
    }
}
employee.setSalary(30000);  // TypeError: employee.setSalary is not a function

we got the TypeError because “employee” can not reference the inner function “setSalary” directly. lets fine tune this code…using closure to solving this problem.

let employee = function(){
	var id = 1010;
	var name = "Akash";
	var salary = 25000;
	var obj = {
		setSalary: function (newSalary){
	        if(isNaN(newSalary)){
	            throw new Error(`${newSalary} is not a number`)
	        }
	        salary = newSalary;
	    },
		getId: function(){return id},
		getName: function(){return name},
		getSalary: function(){return salary},
	}
	return obj;

}();

employee.setSalary(30000)       // this will set salary to 30000
console.log(employee.getId());	// 1010
console.log(employee.getName()); // Akash
console.log(employee.getSalary()); // 30000. because we set the new salary to 30000 through setSalary().

Here we created an object inside the function, added a setSalary method and returned the object. By doing so we introduced setter methods as well. This means whenever the function is called, the object containing the method will be returned, and thus the setSalary method will be accessible to the outer world while protecting the data we do not want to share. Here the important point is, function is immediately invoked and it returned an object whose reference is stored in the employee variable.

In addition to this in the same example to access other data we introduced getters methods. This means all operations which are needed on data will have to be in the form of the methods which will be returned within the object obj.

3. Encapsulation Using Class and Private Variables

Like all major programming languages, we can think of a class as a blueprint. Classes were introduced in ES6 let’s understand with an example.

class employee {
    constructor(id, name, salary){
       this.id = id;
       this.name = name;
       this.salary = salary
    }
    setSalary(salary){
			this.salary = salary;
    }
    getSalary(){
      return this.salary;
    }
}

let emp = new employee(1010,"John", 25000)
console.log(emp.getSalary() )	// 25000
emp.setSalary(55000)
console.log(emp.getSalary()) // 55000

This above solution seems to be a standard solution, but if you watch carefully, you are able to access the properties of this object directly. lets see the below code.

let emp = new employee(1010,"John", 25000);
console.log(emp);   // Output : { id: 1010, name: 'John', salary: 55000 }

This is because, unlike most other languages, data hiding is not inherent to the classes in javaScript. In javaScript by default, properties can be accessed and modified from the outer world. The important point here is properties declared within an object/class are not the same as variables in javascript. You can see this difference in the way object properties are defined (without any var/let/const keyword)

If we start using variables instead of properties, we might just be able to achieve Encapsulation. Since variables are lexically scoped (those declared using var/let/const keyword), they are not accessible from outside the scope they are defined in, hence we can control their access. let’s see in this example.

class employee {
    constructor(id, name, salary){
      let _id = id;
      let _name = name;
      let _salary = salary
      this.getId     = function (){ return _id };
      this.getName   = function (){ return _name };
      this.getSalary  = function (){ return _salary };
      this.setSalary = function (salary){
          _salary = salary
        }
    }

    get id(){
        return this.getId();
    }
    get name(){
        return this.getName();
    }
    get salary(){
        return this.getSalary();
    }
}

let emp = new employee(1010,"John Smith", 25000)

console.log(emp.id )      // 1010
console.log(emp.name )    // John Smith
console.log(emp.salary )  // 25000
emp.setSalary(35000)
console.log(emp.salary )   // 35000

Here we declare the properties within the scope of the constructor instead of defining them as properties at the object level. We then define the constructor and initialize the object properties as variables within the scope of the constructor. In addition we used the get keyword to get the properties. When you initialize the object of the employee class you are able to access the property using {object instance}.name.

Polymorphism in JavaScript

Polymorphism is the presentation of one interface for multiple data types. In the case of OOP, by making the class responsible for its code as well as its own data, polymorphism can be achieved in that each class has its own function that (once called) behaves properly for any object. lets see in this example how to achieve polymorphism.

class Human {
  constructor(name) {
    this.name = name
  }
  sayHi() {
    console.log(`Hi! My name is ${name}`)
  }
}

In this example we created a class called Human and method called as sayHi(). Then you create a Developer and TestEngineer subclass from Human using the extends keyword.

class Developer extends Human () {
  // overwrite the methods. it is now the subclass specific method
  sayHi() {
    console.log(`Hi! My name is ${name}. I am a developer.`)
  }
}

class TestEngineer extends Human () {
  // overwrite the methods. it is now the subclass specific method
  sayHi() {
    console.log(`Hi! My name is ${name}. I am a test engineer.`)
  }
}

Now we have three different classes. class Human is the super class and class Developer, TestEngineer are the subclass. Each one of them has a method sayHi(). We can call sayHi() from each calls instance, and it will produce different results. let’s see in below example.

const cto = new Human('Mark')
const dev = new Developer('Kevin')
const te  = new TestEngineer('Andrew')

cto.sayHi() // Hi! My name is Mark.
dev.sayHi() // Hi! My name is Kevin. I am a developer.
te.sayHi() // Hi! My name is Andrew. I am a test engineer.

We can see that even though the objects are of different classes, they can be treated as if they were objects of the same Human class.

Conclusion

In this blog, we’ve explored some of the key concepts of OOP in JavaScript, including objects, classes, inheritance, encapsulation, and polymorphism. By understanding these concepts and how to use them, we can write more maintainable and reusable code in our JavaScript applications.

We encourage our readers to treat each other respectfully and constructively. Thank you for taking the time to read this blog post to the end. We look forward to your contributions. Let’s make something great together! What do you think? Please vote and post your comments.

Amiya Pattanaik
Amiya Pattanaik

Amiya is a Product Engineering Director focus on Product Development, Quality Engineering & User Experience. He writes his experiences here.