Photo by Joshua Aragon on Unsplash
A Beginner's Guide to Object Oriented Programming in JavaScript
Object Oriented Programming is a popular programming paradigm that is fundamental to many languages such as Java, C++, Python etc.
Object Oriented Programming is a programming paradigm that uses objects as building blocks. According to the Mozilla Developer's Network, Object Oriented Programming is defined as
modeling a system as a collection of objects, where each object represents some particular aspect of the system
This means that the program is broken down into small functionalities that are stored in objects.
In this article, we will be going through how you can structure your code into objects and use the objects to build a robust program.
Prerequisites
Basic JavaScript
Objects
You can check out my articles on Basic JavaScript and Objects
How is Object Oriented Programming Implemented?
Object Oriented Programming is implemented using four main concepts, namely,
Encapsulation
Abstraction
Inheritance
Polymorphism
Using a car example, we will cover these concepts in depth.
Encapsulation
According to the Mozilla Developer's Network, Encapsulation is defined as
Keeping an object's internal state private, and generally making a clear division between its public interface and its private internal state
This means that all the functionalities should be stored in the object to make the code easier to work with.
For example,
You have a Car class that has three properties, a name, a make and mileage. A honda
object is also defined.
class Car {
constructor(name, make, fuelUsed, fullTank, mileage){
this.name = name
this.make = make
this.fuelUsed = fuelUsed
this.fullTank = fullTank
this.mileage = mileage
}
}
const honda = new Car("Civic", "Honda", 15, 41, 1125)
The driver should be able to know how much fuel is left in the tank. Your object currently can not check the amount of fuel remaining. Using the functional approach, you can use the properties and get the values. For example,
function checkFuel(obj){
return `${Math.ceil((1 - (obj.fuelUsed / obj.fullTank)) * 100)}%`
}
console.log(checkFuel(honda)) // (15/41) * 100 = 63%
This code is great and can be reused by many objects. But as the program becomes bigger and has more functionalities, we can forget that this function exists because it is not directly linked to the object.
Alternatively, you can add a method to your object that calculates the percentage of fuel left.
class Car {
constructor(name, make, fuelUsed, fullTank, mileage){
this.name = name
this.make = make
this.fuelUsed = fuelUsed
this.fullTank = fullTank
this.mileage = mileage
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
const honda = new Car("Civic", "Honda", 15, 41, 1125)
console.log(honda.getFuelPercentage()) // 63
Encapsulation is adding functions that use the object's properties into the object. However, the properties and methods are not private. This means that they can be easily accessed and changed by anyone. To avoid this, you can limit the user's access by slightly modifying your code.
Unlike many programming languages, JavaScript does not give us a way to define the scope of variables. However, a common syntax of private properties is to add an underscore(_) at the beginning of the property name.
For example,
class Car {
constructor(name, make, fuelUsed, fullTank, mileage){
this._name = name
this._make = make
this.fuelUsed = fuelUsed
this.fullTank = fullTank
this.mileage = mileage
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
const honda = new Car("Civic", "Honda", 15, 41, 1125)
console.log(honda.getFuelPercentage())
console.log(honda.name) // undefined
console.log(honda.make) // undefined
console.log(honda._name) // "Civic"
console.log(honda._make) // "Honda"
In this example, the name and make have been made private. However, you cannot get their values directly unless you include the underscore before the name.
To solve this, you can use getters to access the property value directly. Getters are methods that return a property's value. They are declared using the get
keyword. Getters allow the user to see the value in the property but they can not update it.
Unlike regular methods, getters are accessed without parentheses. This gives the user the illusion that they are accessing the values directly.
For Example,
class Car {
constructor(name, make, fuelUsed, fullTank, mileage){
this._name = name
this._make = make
this.fuelUsed = fuelUsed
this.fullTank = fullTank
this.mileage = mileage
}
get name(){
return this._name
}
get make(){
return this._make
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
const honda = new Car("Civic", "Honda", 15, 41, 1125)
console.log(honda.getFuelPercentage())
console.log(honda.name) // Civic
console.log(honda.make) // Honda
honda.name = "Fit"
console.log(honda.name) // Civic
honda.make = "Mazda"
console.log(honda.make) // Honda
You can update a property's value using setters.
Setters are methods that get a property's value and change it. They are declared using the set
keyword. Setters are also accessed without parentheses and the value is directly assigned instead of adding it as a parameter.
For example,
class Car {
constructor(name, make, mileage, distanceCovered, fuelUsed){
this._name = name
this._make = make
this._mileage = mileage
this.distanceCovered = distanceCovered
this.fuelUsed = fuelUsed
}
set mileage(mileage){
this.mileage = mileage
}
}
const honda = new Car("Civic", "Honda", 1125)
honda.mileage = 3250
console.log(honda.mileage) //3250
Why is Encapsulation Important?
Encapsulation allows you to easily update your program. In the example above, you can go to the Car class and add a new method without disrupting the program.
Encapsulation gives your code better structure because all the functions are linked to the variables they use.
Abstraction
Abstraction gives you the ability to hide the details and show the essentials. You can store the implementation details in local variables which means the user will not be able to access them.
For example,
class Car {
constructor(name, make, mileage, distanceCovered, fuelUsed){
this._name = name
this._make = make
this.mileage = mileage
this.distanceCovered = distanceCovered
this.fuelUsed = fuelUsed
}
let newMileage = this.distanceCovered / this.fuelUsed
updateMileage(){
this.mileage = newMileage
}
}
const honda = new Car("Civic", "Honda", 1125)
console.log(honda.newMileage) //undefined
The newMileage
can not be accessed as a property as it has been declared as a variable. The mileage formula is constant and the user does not need to know how it is calculated.
Why is Abstraction Important?
- Abstraction helps you hide the important parts of a program that should not be changed.
Inheritance
Inheritance allows you to inherit properties and methods from other objects. Therefore you can create a parent-class Car that has a child class, Fuel.
class Car {
constructor(name, make, mileage){
this._name = name
this._make = make
this._mileage = mileage
}
get name(){
return this._name
}
get make(){
return this._make
}
get mileage(){
return this.mileage
}
set mileage(mileage){
this.mileage = mileage
}
}
class Fuel extends Car{
constructor(name, make, fuelUsed, fullTank){
super(name, make)
this.fuelUsed = fuelUsed
this.fullTank = fullTank
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
const honda = new Fuel("Civic", "Honda", 15, 41, 1125)
console.log(honda.getFuelPercentage())
In the example above, the child class has a different syntax. The extends
keyword allows you to inherit the Car class while the super
keyword tells the program which properties and methods it should give the child class.
You can also use the super
keyword in methods. For example,
class Car {
constructor(name, make, mileage){
this.name = name
this.make = make
this.mileage = mileage
}
get name(){
return this._name
}
get make(){
return this._make
}
get mileage(){
return this.mileage
}
set mileage(mileage){
this.mileage = mileage
}
honk(){
console.log("HOONNNKKK!!")
}
}
class Fuel extends Car{
constructor(name, make, fuelUsed, fullTank){
super(name, make)
this.fuelUsed = fuelUsed
this.fullTank = fullTank
}
honk(){
super()
console.log("MOVE OUT OF MY WAY!!")
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
const honda = new Fuel("Civic", "Honda", 15, 41, 1125)
console.log(honda.getFuelPercentage())
console.log(honda.honk()) //"HOONNNKKK!!" "MOVE OUT OF MY WAY!!"
In this example, we inherit the honk()
method then the class has a new functionality that is added after the inheritance.
Why is Inheritance Important?
- Inheritance reduces code redundancy. If you need to add a new type of car, for example, electric or hybrid, you would inherit the common properties from the Car class.
Polymorphism
According to Wikipedia, Polymorphism is defined as
the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types
Polymorphism allows you to write a single implementation for all objects even when they are created from different classes.
For example,
class Fuel extends Car{
constructor(name, make, mileage, fuelType, fuelUsed, fullTank){
super(name, make, mileage)
this.fuelType = fuelType
this.fuelUsed = fuelUsed
this.fullTank = fullTank
}
honk(){
console.log("MOVE OUT OF MY WAY!!")
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
class Hybrid extends Car{
constructor(name, make, mileage, charged, fuelType, fuelUsed, fullTank){
super(name, make, mileage)
this.charged = charged
this.fuelType = fuelType
this.fuelUsed = fuelUsed
this.fullTank = fullTank
}
honk(){
console.log("BEEEEEPPPPP!!!!")
}
getFuelPercentage(){
return `${Math.ceil((1 - (honda.fuelUsed / honda.fullTank)) * 100)}%`
}
}
class Electric extends Car{
constructor(name, make, mileage, charged){
super(name, make, mileage)
this.charged = charged
}
honk(){
console.log("BEEPP!!!!")
}
}
const cars = []
const civic = new Fuel("Civic", "Honda", 1125, "Diesel", 15, 41)
cars.push(civic)
const camry = new Hybrid("Camry", "Toyota", 875, true, "Petrol", 15, 41)
cars.push(camry)
const modelX = new Electric("ModelX", "Tesla", 565, true)
cars.push(modelX)
for(car of cars){
console.log(car.honk())
}
// "MOVE OUT OF MY WAY!!"
// "BEEEEEPPPPP!!!!"
// "BEEPP!!!!"
In the example above,
There is a main class, Car, which has three sub-classes, namely, Fuel, Hybrid and Electric. Using the three sub-classes, three object instances have been created and stored in the cars array.
You iterate through the array and access the honk()
method for all the objects. The honk()
method acts differently because the objects are made from different classes. The iteration still works even though the implementation is different. This is a great example of polymorphism.
Why is Polymorphism Important?
Polymorphism simplifies the code
Polymorphism allows us to write code once
Summary
Object Oriented Programming is a paradigm that allows you to break down your programs into smaller manageable pieces of code which are stored in objects.
Object Oriented Programming has 4 main concepts; encapsulation, abstraction, inheritance and polymorphism.
Encapsulation is storing a function and the data it uses in an object.
Encapsulation allows you to make properties private so that they cannot be manipulated by other objects.
Getters are methods that return a property's value.
Setters are methods that get a property's value and update it.
Getters and setters are accessed without parentheses, unlike other methods.
Abstraction hides the details and shows the implementation.
Abstraction allows the user to see the result without knowing how to get it.
Inheritance allows you to inherit properties and methods from other classes which reduces code redundancy.
Polymorphism allows you to write code that can work with any object.