blog bg

November 18, 2023

JavaScript: Object-Oriented Programming(OOP)

Share what you learn in this blog to prepare for your interview, create your forever-free profile now, and explore how to monetize your valuable knowledge.

What is Object Oriented Programming(OOP)?

It is a programming paradigm based on the concept of objects and simply means that the style of the code box. As we know already, Objects can contain data(properties), and also code(Methods). So, we can say that, by using Objects, we pack all the data and the corresponding behaviour all into one big block. Hence, the data and the corresponding behaviour  make code more flexible and easier to maintain.

In OOP, objects are self-contained pieces of code, like small applications on their own and we use these objects as building blocks of our application and make objects interact with one another. These interaction happen thorough a public interface, also called API. This interface is basically a bunch of methods that a code, outside of the objects can access and that we use to communicate with objects.

OOP was developed with the goal of organizing code, to make it more flexible and easier to maintain. 


 Classes and Instance

What is Class?

In OOP, we actually need a way to generate new objects from our code. To do that, we use something called classes. We can think of a class as a blueprint, which can be then used to create new objects based on the rules described in the class. So, it's just like an architecture, where the architect develops a blueprint that  describe the plan, which consists a set of rules. From that blueprint, many real houses can then be built in the real world and the classes it's just the same.

Let, take a look at the  fictional User class as an example and I say this fictional because this is not actual JavaScript Syntax. This is just to understand the concept and JavaScript does not support real classes like represented here. We do have a class syntax in JavaScript, but it still works a bit differently.

User{
  username
  password
  email
  
  login(password){
    //login logic
  }
  sendMessage(str){
    //sending logic
  }
}

We can see that, it kind of describes a user who has a username, a password, and an email. So, it's a description of data about a user, but it's not the data itself yet. Because remember, the class is really just a plan and a plan does not contain the real word data just yet. On the other hand, we have the behaviour that associated with the data. In this case, we have login() and sendMessage() methods. As we have seen  earlier, this class has everything(data and behaviour) related to a user. 

Now, let's use this class and actually create a new object from this class. 

For example,

//user 1
new User("John", "aasdasd122w","john@gmail.com")

//user 2
new User("Abraham", "rrtr333ds","abraham@gmail.com")

As you can see that, we have created two users objects like we have in the User class. Now, we call all objects created through a class as instances of that class. An instance is a real object that we can use in our code, which was created from a class. If we see the User class as an blueprint, then an instance is like a real user created from that blueprint. The beauty of the class is that, we can use the class to create as many instances as we need in our application just like we can create multiple users from just one blueprint. All of these instances, we can have different data in them but they all share the same functionality which are login() and sendMessage().

In Short, The class can be used to create actual objects which are called instances and this process of creating an instance is called instantiation.

There are four fundamental principals that can guide us towards a good class Implementation. Which are,

  • Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism
 
What is Abstraction?
 
It is basically means to ignore or hide details that don't matter. This allow us to get an overview perspective of whatever it is, that we are implementing instead of messing with details that don't really matter to our implementation. For Example, we are implementing phone for a user to use. So, without abstraction we could design our class to include everything about the phone like shown below.
 
Phone{
  charge,
  volume,
  valtage,
  temperature
  
  homeBtn(){}
  volumeBtn(){}
  screen(){}
  verifyVoltage(){}
  verifyTemperature(){}
  vibrate(){}
  soundSpeaker(){}
  frontCamOn(){}
  frontCamOff(){}
  rearCamOn(){}
  rearCamOff(){}
}

As you can see that, phone includes all the internal stuff like verifying the phone's temperature and voltage, turning on the vibration motor or the speaker. But as a user interacting with a phone, do we need all of this detail?. Well, We actually don't need. In reality, when we interact with a real phone, all of these details have been hidden away from us as a user and basically only interact with using only the main functions like the home button, volume buttons and the screen and everything else is gone because we simply don't need it as a user.

In short, the phone still needs to vibrate and to measure the voltage or to turn on the speaker, but we can hide these details from the user and simply use those functions without completely understanding it and without having to implement it ourselves. So, that's abstraction.


What is Encapsulation?

It is basically means to keep some properties and methods private inside the class so that, they are not accessible from outside the class. However, some methods can be exposed as a public interface. Let's look at the example which has private properties and method,
User{
  username,
  private password,
  private email
  
  login(password){
    return this.password === password
  }
  comment(text){
    this.checkSPAM(text)
  }
  private checkSPAM(text){
    //verify logic
  }
}

As you can see that, private properties like password and email and checkSPAM() method can not be access outside of the User class. However, inside the class, they are still accessible. So, the public interface is essentially all the methods and properties that are not private, meaning that they are not encapsulated.


What is Inheritance?

Let's say we have two classes User  and Admin. In fact, Admin has all the properties(username, email, password) and methods(login() and comment()) that User has. if we define both classes separately, we will end up with a lot of duplicate code. Well, this is where inheritance comes into play. In OOP, when we have two classes that are closely related, we can have one class inherit from other. So, we will have one parent class and one child class and the child class that extends the parent class. So, a child class inherits all the properties and methods from it's parent class. 

In Short, inheritance makes all properties and methods of a certain class available to a child class, forming a hierarchical relationship between classes.  This allows us to recuse  the logic that is common to both of the classes.


What is Polymorphism?

The Polymorphism, a child class can overwrite a method that it inherited from a parent class

Let's consider User(Parent) and Admin(Child) classes.

User{
  username,
  password,
  email
  
  login(password){
    //login logic
  }
}

Admin{
  username,
  password,
  email,
  permissions
  
  login(password, key){
    //Different login logic
  }
}

As we can see that, both parent and child classes have the  same method login(), but it require different logic in child class. According to polymorphism, login method in the child class will overwrite the login method that has been inherited from the parent class.


How does OOP actually work in JavaScript?

In JavaScript, all objects in JavaScript are linked to a certain prototype. So, the prototype contains methods and properties that are accessible to all objects linked to that prototype. This behaviour is usually called prototypal inheritance. Basically, objects inherit methods and properties from the prototype. 

We have actually seen this mechanism in action many times but without knowing that it was happening, for example, each time we used an array method like map(), we were able to use that method because of prototypal inheritance.  So, when we go to MDN, to check the documentation for any array method let's say map(), what we will see there is actually called Array.prototype.map(). Because Array.prototype is the prototype object of all the arrays that we create in JavaScript. Therefore, all arrays have access to map method.

 As you can see the image, the prototype object contains all the array methods, including map.  For example,

const numbers = [1,2,3,4,5,6]
numbers.map(num => num * 3)

Array.prototype is the prototype of the numbers array, it means numbers array is linked to that prototype and therefore, it has access to all the methods that are defined on the Array.prototype object, just like the map() method. We can say that, numbers array inherits the map() method because of the prototypal inheritance, it's basically an instance inheriting from a class, but what matters is that, the map method is actually not defined on the numbers array but on it's prototype.

 

There are three ways of implementing prototypal inheritance in JavaScript which are,

  • Constructor function technique

The constructor() functions are a way of creating objects programmatically, using a function which will also set the new object's prototype. This is how built-in objects like arrays or maps or sets are implemented.

In JavaScript, a constructor gets called when an object is created using the new keyword. The purpose of a constructor is to create a new object and set its values for any existing object properties.

  • ES6 classes

ES6 classes are an modern alternative to the constructor function syntax to implement prototypal inheritance. However, behind the scene, ES6 classes are actually implemented with constructor functions

  • Object.create()

It is basically the easiest and most straightforward way of linking an object to a prototype object. This method returns a new object with the specified prototype object and properties.


 1. Constructor functions and the new operator

We can use constructor functions, to build an object. A constructor() function a completely normal function. The only difference between a regular function and constructor function, is that we call a constructor function with the new operator. Also, in OOP name of the constructor function starts with a capital letter. Other  built-in constructors like array or map, follow that convention as well.

We can use either function expression or function declaration to create a constructor function, but  an array function will not actually work as a function constructor, because it does not have it's own keyword 

When call the constructor() function using the new keyword, behind the scenes, there have been four steps which are

  1.  A new empty object({}) is created.
  2. After the function called, this keyword will be set to the newly created and will point to the object created in step 1
  3.  The newly created object is linked(__proto__) to the constructor function's prototype property
  4. The object was created in the beginning is then automatically returned from the constructor function

Let's create a constructor function for a person using function expression and this is basically going to create an object for a person.

const Person = function (firstName, lastName, birthYear){
  console.log(this);
}

new Person("Simon","Fleming",2001)

Output:

Person {}

Output

As you can see the output, empty object that was created in the step 1 was returned. That's because we are calling it with the new keyword. Also, return empty object is a type of Person which is just a name of the constructor function. So, whatever we we add to that empty object, that we are trying to build. 

 

So, all we need to do is to take the firstName parameter, then create a property on this keyword with the same name and then set it to that value and we will do the same with lastName and birthYear parameters like shown below. 

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

const person1 = new Person("Simon","Fleming",2001)
console.log(person1);

Output:

Person { firstName: 'Simon', lastName: 'Fleming', birthYear: 2001 }

As you can see now, we got a person object with first name, last name and birth year that we passed onto the constructor function.

So again, we are calling the Person constructor() function with the new keyword and therefore, a new empty object is created. When the function is called, in the function, we start to set the properties to that object, using this keyword and we set the exact same name as we are passing into the constructor function. However, it does not have to be the same name as our arguments or parameters, but this is kind of convention that if we pass firstName, then we should also create a property called firstName. Now, we can create this constructor function to create as many different objects as we want.

Person Simon is an instance of the Person. To test, there is an operator to check if the person simon is an Instance of the Person.

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

const person1 = new Person("Simon","Fleming",2001)
console.log(person1 instanceof Person);//true

Also, firstName, lastName and birthYear properties are also called instance properties, because they will be available on all the instances that are created through the constructor function.

What if we wanted to add method to the Objects?. Well, just like we added properties, we can also add methods. For example,

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName
  this.lastName = lastName
  this.birthYear = birthYear
  
  this.calcAge = function () {
    return 2023 - this.birthYear
  }
}

const person1 = new Person("Simon","Fleming",2001)
console.log(person1.calcAge());//22

This would work fine, but this is actually a bad practice. So, we should never create a function inside of a constructor function. That's because, image we are going to create thousands of person objects using this constructor function. Then what would happen, is that each of these objects would carry the function we created inside the constructor function. So, if we had thousands of person objects, we would essentially create a thousand copies of the calcAge() function, that would be terrible for the performance of our code.

To solve this problem, we are going to use prototypes and prototype inheritance, meaning that, we can define a function on the prototype of an object directly.

 

Prototypes

Each and every function in JavaScript automatically has a property called prototype and that includes, of course, constructor functions and every object that's created by a certain constructor function will get access to all the methods and properties that we define on the constructors prototype property.

So, just to visualize, in our case this would be Person.prototype. All the objects that are created through the  Person constructor() function, will inherit all the methods and properties that are defined on the prototype property. 

Let's add a method to the prototype property,

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

const person1 = new Person("Simon","Fleming",2001)


Person.prototype.calcAge = function () {
  return 2023 - this.birthYear
}

console.log(Person.prototype);

If we visualize the output of Person.prototype,

As you can see that, we already have a calcAge() method in there. So, every object that is created from the Person constructor function will have access to calcAge() method. For example,
const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

const person1 = new Person("Simon","Fleming",2001)


Person.prototype.calcAge = function () {
  return 2023 - this.birthYear
}

console.log(person1.calcAge());//22

As you can see that, person1 was created from the Person constructor() function. So, we can now use this method on the person1 object even though it is not really on the object itself. So if we check the person1 object,

We can see that, person1 object contains firstName, lastName and birthYear properties but it does not contain the calcAge() method. But, it still has an access to it because of prototypal inheritance. So, this approach will solve the problem of adding the calcAge() method directly to each of the objects that would create a copy of the method and attached to every single object. With this solution, now there is only one copy of the calcAge() function exists, then all the objects that are created using the Person constructor function can reuse this function on themselves. So, the this keyword, always set to the object that is calling the method.
Each object has a special property called __proto__ and using that  property, we can access the prototype of the constructor like this,
console.log(person1.__proto__);

Output

This is the prototype of person1 and we are able to see the calcAge() function, thats why, we are able to use it with the person1 object. So the propotype of person1 object is essentially the prototype property of the constructor function. Let's check that,
console.log(person1.__proto__ === Person.prototype);//true

As we can see that, person1's prototype is the prototype property of the Person constructor function. So the Person.protoype is now the person1's prototype which is denoted in the __proto__ and this will always point to an object prototype.

However, The prototype of the Person is the prototype of person1 and it does not belongs to Person, which means it is not the prototype of Person. Let's check that,

console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(Person));//false

So, at this point we might think that, where does __proto__ on the person1 object come from?. Well, we have already discuss that when we call the constructor function with new operator, there have been four steps and if you look at the step 3, which links the empty new object to the prototype. So, the step 3 will create the __proto__ property and sets the proto property on the object to the prototype property of the constructor function. This is how, JavaScript knows that the person1 object is connected to Person.prototype.

In addition to the methods, we can also set properties on the prototypes. For example,

Person.prototype.gender = "Male"

Output

However, gender property is not directly on in the object because it is not it's own property. To check the gender property is the direct property or inherited property we can use hasOwnProperty() method. For example, 

console.log(person1.hasOwnProperty("firstName"));//true
console.log(person1.hasOwnProperty("gender"));//false

If a property or function  can not be found in a certain object, JavaScript will look into it's prototype and this behaviour is what we called prototypal inheritance. So, person1 object inherits the gender property and calcAge() method from it's prototype.

Person.prototype is the prototype of person1 => prototype of person1 which is exactly the prototype property of Person

person1 object is connected to a prototype and ability of looking up methods and properties in a prototype is what we called the prototype chain.

 

Prototypal Inheritance on built-on Objects.

When we create a new array, it inherits all the available methods from Array.prototype because of prototypal inheritance. For example, we create an array and if we take a look at the __proto__ property of that array,

const arr = [1,2,4,5,6,7];
console.log(arr.__proto__);

Output


We can see all the array methods that we already know are in the arr's prototype. This is the reason why all the arrays get access to all of those array methods and each array we create, does not contain all these methods but instead, the will inherit these methods from it's prototype.

we can also check if that arr's prototype which is exactly the Array.prototype.

console.log(arr.__proto__ === Array.prototype);//true

NOTE:- So, the prototype property of the constructor is going to be the prototype of all the objects created by that prototype

If we log arr in the console, we will get something like this,


 We can see the prototype property of the arr, where we can see all the array methods. For example, if we check the official documentation for map() method, we will actually see the name of the method is Array.prototype.map(). This is because this map() method lives in the prototype property of the Array constructor. So, all of the array methods exist only once somewhere in the JavaScript engine and then all the array in our code get access to the array methods through the prototype chain and prototypal inheritance.

We can also attach our own array method to Array.prototype so that whenever we create a new array, it will have access to the function we created. For example, we are going to create a method that will returns all the unique elements of an array.

const arr = [1,2,4,4,5,5,6,7];

Array.prototype.unique = function(){
  return [...new Set(this)]
}
console.log(arr);//[1, 2, 4, 4, 5, 5, 6, 7]
console.log(arr.unique());// [1, 2, 4, 5, 6, 7]

It worked. we added a method called unique() to the prototype property of the array constructor and therefore, now all arrays will inherits this method. So, if we console arr again, we will be able to see the unique() method on the arr's prototype property.


However, extending the prototype of a built-in object is generally not a good idea because, for example, next version of the JavaScript might add a method with the same name that we are adding and that might work in different way and also then our code will start using the new method instead of the method that we create, then that will probably break our code.

 

2. ES6 Classes

When we create classes in OOP, they still implement prototypal inheritance behind the scenes.

Let's implement Person using a class,

  • Using class declaration
class Person {
  
}
  • Using class expression
const Person = class {
  
}

Although we use the class keyword, behind the scenes, classes are still functions and therefore we have class expressions and class declarations. It's completely up us to choose either one according to our preference.

Inside the class, the first thing we need to do is to add a constructor() method. This constructor actually works in a pretty similar way as a constructor function. Also, we can pass arguments for the properties that we want the object to have like in constructor functions. For example,

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

So, whenever we create an object using new operator, this constructor will automatically called. In other words, whenever we create an object of a class, constructor() is called first.

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

const person1 = new Person("Simon","Fleming",2001);
console.log(person1);

Output:

Person { firstName: 'Simon', lastName: 'Fleming', birthYear: 2001 }

So basically, when we create a new instance, then the constructor is invoked first and that will return a new object, that will be stored into person1. As you can see, when log into the console, we are able to see the properties of the person1.

Let's add a method to the class,

const Person = class {
  constructor(firstName,lastName,birthYear){
    this.firstName = firstName
    this.lastName = lastName
    this.birthYear = birthYear
  }
  
  calcAge(curYear){
    return curYear - this.birthYear
  }
  
}

const person1 = new Person("Simon","Fleming",2001);
console.log(person1.calcAge(2023));//22

It is important to understand that, all of the methods that we write in the class will be added to the prototype of the objects but not on the objects themselves. 

There are some important things to remember about classes. which are,

  • classes are not hoisted:- we can't use them before they are declared in the code.
  • Classes are also first-class citizens:- we can pass them into functions and also return them from function.
  • Classes are executed in strict mode:- even if we didn't activate it for our entire script, all the code that is in the class will be executed in strict mode.

NOTE:- Whenever we create an object of a class, if there is no constructor, the default constructor will be called and which does nothing

 

Setters and Getters

Setters and getters are basically functions that set and get a value, but on the outside, they still look like regular properties. Let's take a look at getters and setters.

const Person = class {
  constructor(fullName){
    this.fullName = fullName
  }
  
  set fullName(fullName){
    this._fullName = fullName
  }
  
  get fullName(){
    return this._fullName
  }
  
}

const person1 = new Person("Simon Fleming");
person1.fullName = "Herbie Johnston"
console.log(person1.fullName);//Herbie Johnston

So, when we have a setter, which is trying to set a property, we have to use underscore(_) before the name of the property as a convention. Otherwise, we will get an error because both constructor and setter function trying to set the exact same property name. So, whenever we try to set a property that already exists, we just need to use underscore(_) before the name of the property to avoid collision which will course error.

 

Static methods

A good example to understand what a static method actually is, is the built-in function called Array.from() method, which converts any array like structure to a real array. The from() method is attached to the Array constructor, so we could not use the from() method on any array we create. So that, all the arrays we create will not inherit this from method because it is not in their prototype and simply attached to the constructor itself. 

So, static methods are bound to a class and not to the instances of class and also we can only access them through the classes.

we have two ways of implementing the static methods.

  • Create directly from the class
const Person = class {
  constructor(firstName,lastName,birthYear){
    this.firstName = firstName
    this.lastName = lastName
    this.birthYear = birthYear
  }
  
  calcAge(curYear){
    return curYear - this.birthYear
  }
}

const person1 = new Person("Simon","Fleming",2001);

Person.greeting = function(){
  return `Hi! Welcome to OOP`
}
console.log(person1.greeting());//it will throw an Error
console.log(Person.greeting());//Hi! Welcome to OOP
  • Create using static keyword
const Person = class {
  constructor(firstName,lastName,birthYear){
    this.firstName = firstName
    this.lastName = lastName
    this.birthYear = birthYear
  }
  //instance method
  calcAge(curYear){
    return curYear - this.birthYear
  }
  //static method
  static greeting(){
    return `Hi! Welcome to OOP`
  }
}

const person1 = new Person("Simon","Fleming",2001);

// console.log(person1.greeting());//it will throw an Error
console.log(Person.greeting());//Hi! Welcome to OOP

NOTE:- Static methods are not available on the instances

 

3. Object.create()

This is the one of the ways to implementing prototypal inheritance. However, this static method works in a different way than constructor() functions and ES6 classes work. This function uses the idea of prototypal inheritance but, there are no  prototype properties involves and no constructor functions and new operators. Instead, we can use Object.create() to manually set the prototype of an object to any other object that we want.

For example,

When we create person1, we will pass the in the object that we want to be the prototype of the new object that is Person. So, it will now return a brand new object, that is linked to the Person object, which will be it's prototype. So, person1 is right now is empty object like shown below and will be linked to the Person object,

const Person = {
  calcAge(){
    return 2023 - this.birthYear
  }
}

const person1 = Object.create(Person);
console.log(person1);//{}
We can see that, person1 is empty, but we have the prototype, where we have calcAge() function, but now we don't have any properties on the object yet. Let's add a property,
const Person = {
  init(firstName, lastName, birthYear){
    this.firstName = firstName;
    this.lastName = lastName;
    this.birthYear = birthYear;
  },
  calcAge(){
    return 2023 - this.birthYear
  },
}

const person1 = Object.create(Person);
person1.init('Simon', 'Fleming', 2001);


console.log(person1);//{ firstName: 'Simon', lastName: 'Fleming', birthYear: 2001 }
console.log(person1.calcAge());//22

the init() function looks similar to the constructor function that we used earlier, but init() has noting to with any constructor function, because we are not using the new operator to call, we simple call person1.init

console.log(person1.__proto__ === Person);//true

So, Person should be the prototype of person1

NOTE: Object.create() creates a new object, and the prototype of that object will be the object that we passed in.

We have another way of passing properties to the object through Object.create(). For example,

const Person = {
  calcAge(){
    return 2023 - this.birthYear
  },
}

const properties = {
  firstName:{
    value:'Simon'
  },
  lastName:{
    value:'Fleming'
  },
  birthYear:{
    value:2001
  }
}

const person1 = Object.create(Person,properties);
console.log(person1.calcAge());//22

 

Inheritance between classes

Inheritance allows us to create a class/object that takes all the functionalities from a parent class/object and allows the child to add more own functionalities. For example, we will create a Student class(child) and make it inherit from the Person class(parent) that's because Student is a subtype of Person. With this inheritance set up, we can have specific methods for student, but the student can also use generic Person methods. 

That's the basic idea that we are going to implement using constructor functions, ES6 classes and Object.create()

 

1. Constructor Function

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

Person.prototype.calcAge = function () {
  return 2023 - this.birthYear
}

const Student = function (firstName, lastName, birthYear,course){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
  this.course = course
}

Student.prototype.introduce = function () {
  return `My name is ${this.firstName} and ${this.lastName} and I am studying ${this.course}`;
}

const student1 = new Student("Mike","Johnson", 2002, "CSC")
console.log(student1.introduce());//My name is Mike and Johnson and I am studying CSC

As you can see that, Student class has kind of the same data as Person class. The Student class all the properties that Person class and it has an additional property as well. However, we have to improve the Student constructor function, because it is actually a copy of Person constructor. Having duplicate code is never a good idea, because it violates the DRY(Don't Repeat Yourself) principal and if the implementation of Person changes in the future, then  that change will not be reflected in the Student.

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

Person.prototype.calcAge = function () {
  return 2023 - this.birthYear
}

const Student = function (firstName, lastName, birthYear,course){
  Person.call(this, firstName, lastName, birthYear)
  this.course = course
}

Student.prototype.introduce = function () {
  return `My name is ${this.firstName} and ${this.lastName} and I am studying ${this.course}`;
}

const student1 = new Student("Mike","Johnson", 2002, "CSC")
console.log(student1.introduce());//My name is Mike and Johnson and I am studying CSC

As you can see that, in the child class we called the parent class with it's properties after set the  this keyword. To do that, we used the call() method to specify the this keyword as first argument. Once we did this, we were able to access the firstName, lastName and birthYear properties from the parent class.

We actually want the Student class to be the child class and inherits from the Person class. So that, the Student class could also get access to methods from the Person's prototype property. However, what we did so far is that, the child class has access to all the properties of the parent class but it does not have access to the methods of the parent class as we have not connected the prototype of Person and Student.

To do that, we are going to use Object.create(), because defining prototypes manually is exactly what Object.create() does.

Student.prototype = Object.create(Person.prototype)

With this, the Student.prototype is now an objects that inherits from the Person.prototype. After this, we have have connected the prototype of Person and Student. However, we will have to create this connection before we add any more methods to the prototype object of Student because Object.create() will return an empty object and that point, Student.prototype is empty. Suppose if we create Student.prototype.introduce() first, before creating the connection, then Object.create() would overwrite these methods which means, removing all the methods that we had already added to the prototype object of Student.

Here is the complete code,

const Person = function (firstName, lastName, birthYear){
  this.firstName = firstName;
  this.lastName = lastName
  this.birthYear = birthYear
}

Person.prototype.calcAge = function () {
  return 2023 - this.birthYear
}

const Student = function (firstName, lastName, birthYear,course){
  Person.call(this, firstName, lastName, birthYear)
  this.course = course
}
//linking prototypes
Student.prototype = Object.create(Person.prototype)

Student.prototype.introduce = function () {
  return `My name is ${this.firstName} and ${this.lastName} and I am studying ${this.course}`;
}

const student1 = new Student("Mike","Johnson", 2002, "CSC")
console.log(student1.introduce());//My name is Mike and Johnson and I am studying CSC
console.log(student1.calcAge());//21

if we look at the Student prototype, 

console.log(student1.__proto__);

 

console.log(student1.__proto__.__proto__);;


As you can see that, when we call calcAge() function on Student class, the calcAge() method is not directly on the student1 object(student1.__proto__) where, we defined only the introduced method, but not calcAge(). Whenever we try to access a method, that's not on the object's prototype, then JavaScript, will look up even further(student1.__proto__.__proto__) in the prototype chain and see if it can find a method, which is in the parent prototype.Then, JavaScript will finally find the calcAge() in Person.prototype, where we defined it and that's the whole reason why we set up the prototype chain like this so that, the student1 object can inherit whatever methods are in its parent class, basically.

 

2. ES6 classes

To implement inheritance between ES6 classes, we will use two extends keyword and super function.

class Person {
  constructor(firstName, lastName, birthYear){
      this.firstName = firstName;
      this.lastName = lastName
      this.birthYear = birthYear
  }
  
  calcAge() {
    return 2023 - this.birthYear
  }
}


class Student extends Person{
  constructor(firstName, lastName, birthYear,course){
      super(firstName, lastName, birthYear);
      this.course = course
  }
  introduce(){
    return `My name is ${this.firstName} and ${this.lastName} and I am studying ${this.course}`;
  }
}


const student1 = new Student("Mike","Johnson", 2002, "CSC")
console.log(student1.introduce());//My name is Mike and Johnson and I am studying CSC
console.log(student1.calcAge());//21

As you can see that, to make Student class to inherit from Person class, we just need to say Student class extends Person class. So, the extends keyword will link the prototypes behind the scenes.

However, to make child class to use it's parent class properties, we just need to call the super() function. This super() function is actually the constructor function of the parent class. It is important that, the super() function needs to be called before accessing the this keyword because super() function is responsible for creating the this keyword in the subclass. So therefore, without doing this, we wouldn't be able to access the this keyword to do this. So, always first the call to the super(), which will call to the parent's class constructor and from there we will be able to access the this keyword. Suppose, if the child class does not have any own properties and just have to use the parent class's properties, then we don't have to call constructor function on class because super() function will automatically be called with all the arguments that we pass. So, if we don't need any new properties, then we don't need to write a constructor method in the child class.

We can also override the parent method by implementing a method with the same name in the child class. For example,

class Person {
  constructor(firstName, lastName, birthYear){
      this.firstName = firstName;
      this.lastName = lastName
      this.birthYear = birthYear
  }
  
  calcAge() {
    return 2023 - this.birthYear
  }
}


class Student extends Person{
  constructor(firstName, lastName, birthYear,course){
      super(firstName, lastName, birthYear);
      this.course = course
  }
  introduce(){
    return `My name is ${this.firstName} and ${this.lastName} and I am studying ${this.course}`;
  }
  calcAge() {
    let age = 2023 - this.birthYear
    return `I'm ${age} years old!`
  }
}


const student1 = new Student("Mike","Johnson", 2002, "CSC")
console.log(student1.calcAge());//I'm 21 years old!

As you can see that, calcAge() function from parent class has been overridden by the child class.

 

3. Object.create()

To make Student to inherit direct from the Person, we created Student object that will be pointing to Person with the help of Object.create and which will make Person will actually become the prototype of Student and allows Student to inherits from Person. At that point, Student object will be empty and from now we will be able to create new students. 

For example,

const Person =  {
  init(firstName, lastName, birthYear){
      this.firstName = firstName;
      this.lastName = lastName
      this.birthYear = birthYear
  },
   calcAge() {
    return 2023 - this.birthYear
  }
}

const Student = Object.create(Person);
Student.init = function(firstName, lastName, birthYear, course){
  Person.init.call(this, firstName, lastName, birthYear)
  this.course = course;
}

  Student.introduce = function (){
    return `My name is ${this.firstName} and ${this.lastName} and I am studying ${this.course}`;
  }

const student1 = Object.create(Student);
student1.init("Mike","Johnson", 2002, "CSC");

console.log(student1.introduce());//My name is Mike and Johnson and I am studying CSC
console.log(student1.calcAge());//21


 

Encapsulation

Encapsulation is basically to keep some properties and methods private inside the class so that, they are not accessible from outside of the class and then the rest of the methods are basically exposed.

There is a reason why we need encapsulation, is to prevent code from outside of the class to accidentally manipulate the data inside the class.

Let's consider following class,

class BankAccount{
  constructor(owner, currency, pin){
    this.owner = owner
    this.currency = currency
    this.pin = pin
    this._transactions = []
  }
  
  deposit(val){
    this._transactions.push(val)
  }
  withdraw(val){
    this.deposit(-val)
  }
}


 

Protected Properties and methods

As you can see the class, we need to protect transactions array which contains the account's deposits and withdrawal. We will have to protect this data so that, no one will accidentally manipulate the data. To do that, we just need to add underscore(_) in front of the property and this is called protected property, meaning that, a protected member is accessible within the class and any object/class that inherits from it.

However, We can still access this protected property outside of the class. This is just a convention that programmers use. 

For example,

class BankAccount{
  constructor(owner, currency, pin){
    this.owner = owner
    this.currency = currency
    this._pin = pin
    this._transactions = []
  }
  
  deposit(val){
    this._transactions.push(val)
  }
  withdraw(val){
    this.deposit(-val)
  }
  _approveLoad(value){
    return true;
  }
  
  getTransactions(){
    return this._transactions;
  }
}

const account1 = new BankAccount("Mike", "GBP", 1234)
account1.deposit(2000);
account1.withdraw(1500);
account1.deposit(1800);
console.log(account1.getTransactions());//[ 2000, -1500, 1800 ]
account1._transactions.push(5000);
console.log(account1.getTransactions());//[ 2000, -1500, 1800, 5000 ]

As you can see that, _transactions is still accessible outside of the class but at least everyone in the team knows that, this variable is not supposed to be touched outside of the class.

 

Private Properties and methods

To implement a truly private property in JavaScript we have to use (#) in front of the property name or method. These private properties and methods will not be accessible from outside of class which will make them truly private.

This will help restrict accessing properties from outside of the class. If we want to access property from outside we have to make method, which will only print properties without giving access to change value of that property.

For example,

class BankAccount{
  #pin
  #transactions = []
  
  constructor(owner, currency, pin){
    this.owner = owner
    this.currency = currency
    this.#pin = pin
  }
  
  deposit(val){
    this.#transactions.push(val)
  }
  withdraw(val){
    this.deposit(-val)
  }
  #approveLoad(value){
    return true;
  }
  
  getTransactions(){
    return this.#transactions;
  }
   getPin(){
    return this.#pin;
  }
  requestLoad(val){
    if(this.#approveLoad(val)){
      return "Load approved"
    }
  }
}

const account1 = new BankAccount("Mike", "GBP", 1234)
account1.deposit(2000);
account1.withdraw(1500);
account1.deposit(1800);
console.log(account1.getTransactions());//[ 2000, -1500, 1800 ]
console.log(account1.getTransactions());//[ 2000, -1500, 1800, 5000 ]
console.log(account1.#transactions);//it will throw an error as it is private property
console.log(account1.getPin());//1234
console.log(account1.#pin);//it will throw an error as it is private property
console.log(account1.#approveLoad(200)); //it will throw an error as it is private method
console.log(account1.requestLoad(200));//Load approved


 Chaining Methods

Lets consider above BankAccount class,

In this class, we want to do following operation like shown below,

account1.deposit(2000).withdraw(1500).deposit(1800);

In arrays, We have already chained array methods one after another, for example filter map and reduce? So by chaining these methods, we could first filter an array, then map the result. And finally reduce the results of the map, all in one line of code.

We can actually implement the same ability of chaining methods in the methods of our class. Actually, this is extremely easy to do. So, all we have to do is to return the object itself(this) at the end of a method that we want to be chainable.

For example,

class BankAccount{
  #pin
  #transactions = []
  
  constructor(owner, currency, pin){
    this.owner = owner
    this.currency = currency
    this.#pin = pin
  }
  
  deposit(val){
    this.#transactions.push(val)
    return this;
  }
  withdraw(val){
    this.deposit(-val)
    return this;
  }
  #approveLoad(value){
    return true;
  }
  
  getTransactions(){
    return this.#transactions;
  }
   getPin(){
    return this.#pin;
  }
  requestLoad(val){
    if(this.#approveLoad(val)){
      return "Load approved"
    }
  }
}

const account1 = new BankAccount("Mike", "GBP", 1234)
account1.deposit(2000).withdraw(1500).deposit(1800);

console.log(account1.getTransactions());//[ 2000, -1500, 1800 ]


 

ES6 classes Summary

class Student extends Person{
  university = "University of London"
  #studyHours = 0
  #course
  static numSubjects = 10;
  
  constructor(firstName, birthYear, startYear, course){
    super(firstName, birthYear)
    this.startYear = startYear
    this.#course = course
  }
  
  introduce(){
    return `I study ${this.course} at ${this.University}`
  }
  
  study(h){
    this.makeCoffe()
    this.studyHours += h
  }
  
  #makeCoffe(){
    return "Here is a coffee for you"
  }
  
  get testScore(){
    return this._testScore
  }
  
  set testScore(score){
    this._testScore = score
  }
  
  static printCurriculum(){
    return `There are ${this.numSubjects} subjects`
  }
}

const student1 = new Student("Mike", 2000, 2023,"CSC")

Let's quickly all the terminology around Student class. We usually define a class Student with class keyword and  it is a child class because it extends the parent class called Parent class using extends keyword, which will setup inheritance between these two classes. Also, the extends keyword will automatically set up the prototype chain for us.

university property is a public field that is very similar to a property that we defined in a constructor. We have also private fields which are studyHours and course. They are not accessible outside of the class and this is perfect for implementing encapsulation. We also have static public fields but they are available only on the class meaning that, they can not accessed on the instances that created from the class. we use static keyword to make any field static.

In constructor function, it will automatically called by the new operator whenever, we create a new instance of the class. This constructor method is mandatory in any regular class, but it might be omitted in a child class if we want it to have the exact same number of parameters that the parent class has. Then inside of the constructor, there is a call to the parent class, that is the super() method. This is only necessary, whenever we are writing a child class and this function needs to be called before we access the this keyword in the constructor.

We also have instance properties(startYear and course) and just like public fields, these properties are also available on each created object. But, the difference between instance property and public property is that, we set the instance properties based on input data of the constructor and they are unique for each object while public fields are usually common to all objects. For example, university field is not unique to each student object. 

As you can see that, with private properties, we have also private methods(#makeCoffe()) and they are only available on the class. Just like static properties, we use static keyword with function to make them  static function. Since, they are available only on class, they can not access the instance properties or methods but only the properties or method which are static. As you can see the Student class, static field numSubjects will be accessible in the static method called printCurriculum().

Class also have getters and setters. The getter method is basically to get a value out of an object by simply writing a property instead of writing method(student1.testScore) and the same for the setter method where, we simply define the test score by setting it to some value instead of calling a testScore() method(student.testScore = 50). Keep in mind that, if you have a setter for a property that is already defined in the constructor, then we need to create a new property with the (_) in front of it. 

Finally, we can create an instance of Student class using new keyword.

Next ArticleJavaScript: DOM and Events Fundamentals

 

356 views

Please Login to create a Question