Published on

OOP in JavaScript

Authors
  • avatar
    Name
    Igor Tosic
    Twitter
OPP-in-JavaScript

OOP in JavaScript

The beauty of JavaScript is multi-paradigm. We can use both functional and object-oriented approaches to make our code cleaner, more understandable, easy to extend and more efficient. We can pick the paradigm for the specific problem based on our issues.

When we are talking about OOP in JavaScript, we need to cover things like this keyword, prototype inheritance, the new keyword, the ES6 classes, object, create, private and public fields and four principles of OOP.

We know in OOP, we have two main types: class-based programming languages and prototype-based programming languages. In JavaScript, we have prototype inheritance. Object-oriented programming is all about modelling real-world objects and relationships.

We can start a few steps behind OOP in JavaScript and see how things are doing with functions and objects.

const person = {
    name: “igor”,
    hobby: “basketball”,
    Doing() {
        return name + " is love to play " + hobby
    }
}

What did we see, and what are the benefits of that!? We grouped functionality. We have states and functions or methods of doing something. We have something called encapsulation. That is our first step in object-oriented programming.

That is fine, but what is the problem with this!? So, if we want to create person1, we need to copy the previous object, and so on.

What we can do here is to create a factory function. Factory function is the function that creates an object and returns it. It is like a constructor or class function, but we will see that later. So, we are moving a step forward to OOP.

function createPerson(name, hobby) {
  return {
    name,
    hobby,
    doing() {
      return name + ' is love to play ' + hobby
    },
  }
}

const igor = createPerson('Igor', 'basketball')

console.log(igor.doing())

Here, we avoided repeating code, but still, we have a problem. In case we have thousands of persons, they require space in memory. Things like name and hobby are different, and that’s okay, but methods of doing them are standard and the same thing. That is going to copy on memory for each person. And this could be better.

We have something in JavaScript; how can we solve this problem? It is prototype inheritance, but now we will see how to solve it manually.

The easiest way to get a method outside of function and make the new object:

const doingFunc = {
  doing() {
    return name + ' is love to play ' + this.hobby
  },
}

So now, if want to create a new method inside our createPerson we just do:

igor.doing = doingFunc.doing

Actually, here we can use Object.create() and simplify things. The Object.create() static method creates a new object, using an existing object as the prototype of the newly created object.

function createPerson(name, hobby){
  let newPerson = Object.create(doingFunc)
  newPerson.name = name
  newPerson.hobby  = weapon
  return newPerson
}

So, what does an object create do for us!? It links doingFunc and newPerson. We are doing prototype inheritance here.

If you try to log newPerson inside of createPerson like this console.log(newPerson) you will get the empty object, but not doing method from doingFunc. You need to write console.log(newPerson.__proto__)

Object.create() solved our problems. We are using prototype inheritance; everything is working, and we are doing things.

There are not too many code bases with this approach; this is not the standard way of doing things inside the JavaScript community.

Things look closer to OOP, but we still need to fundamental OOP in JavaScript.

Now we will see something we had before Object.create().

Before we didn't have Object.create(), we used constructor functions. Something like this:

function Person(name, hobby) {
  this.name = name;
  this.hobby = hobby;
}

You can notice we don't return anything. We are using the keyword this, and that's it. We are constructing. But if we try to create an object from this as we did previously, you will get an error, that prop. is undefined.

const igor = Person('Igor', 'basketball')

To use constructor function you need to use new keyword in JavaScript.

const igor = new Person('Igor', 'basketball')

That is just a way JavaScript works :). The new keyword automatically returns the object, creating a Person constructor. It runs our code and constructs the Person function. So, we can say that any function invoked by new keyword is called a constructor function. As a rule, all constructor functions should start with a capital letter to let other programmers know that the function should be called with new keyword.

One note about this and new keyword: We know that this in JavaScript points to the global object, the window object. But new keyword changes it. When you use new keyword, we are pointing this to the object we just created. Because of that, we got an error when we tried to create an object without a new keyword.

Every function in the JavaScript script automatically gets a prototype property. As we know, everything is an object in JS; functions are special type of objects. __proto__ is useful for constructor functions. So, we can simply do this to get the method attached to the function.

Person.prototype.doing = function(){
  return "I like to " + this.hobby
}

Also, one more note about arrow functions. If we try to write like this, we will get undefined:

Person.prototype.doing = () => {
  return "I like to " + this.hobby
}

This is because arrow functions are lexically scoped.

One more important thing about prototyping in JS: If we have the example below and try to log console.log(igor.prototype), we will get undefined. Why!? Because igor is not a function, it is an object, and only functions have access to the prototype.

function Person(name, hobby) {
  this.name = name;
  this.hobby = hobby;
}

Person.prototype.doing = function(){
  return "I like to " + this.hobby
}

const igor = new Person('Igor', 'basketball')
console.log(igor.prototype)

If we try to make our method more complex, like in this example, we need to take care about one thing. About this keyword.

function Person(name, hobby) {
  this.name = name;
  this.hobby = hobby;
}

Person.prototype.doing = function(){
  function something(){
    return this.hobby + " is the best for me"
  }
  something();
}

const igor = new Person('Igor', 'basketball')
console.log(igor.doing())

It will be undefined. Function inside methods, or just functions inside functions, means that this keyword inside function is not assigned to object itself, but actually to window object. Solution is to bind function.

Person.prototype.doing = function(){
  function something(){
    return this.hobby + " is the best for me"
  }
  something().bind(this);
}

Or :)

Person.prototype.doing = function(){
  const self = this;
  function something(){
    return self.hobby + " is the best for me"
  }
  return something();
}

With this and new keyword coding style, the idea is much more object-oriented programming. But things like Object.create() simplify things in JS, and they come out. Object.create() is technically less OOP than we have with constructor functions. This construction approach is not pretty and is sometimes hard to understand and follow. We are moving to our goal, to add classes :)

Finally, we are here ES6 and classes. ECMAScript 2015 was the second major revision to JavaScript. ECMAScript 2015 is also known as ES6 and ECMAScript 6. Here, we got let and const keywords, Promises and many more. But we are talking about classes now :).

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

Constructor is something that gets run every time when we use new keyword when creating new object.

Alo this line of code:

Person.prototype.doing = () => {
  return "I like to " + this.hobby
}

we are going to move to our class.

class Person {
  constructor(name, hobby){
    this.name = name;
    this.hobby = hobby;
  }
  doing(){
    return "I like to " + this.hobby
  }
}

When we have any updates, we put things inside class. Class becomes some blueprint for what we want to do. And when we update methods or change something inside a class, all instances will get updated. We are now using the word instance, which is a common term for object-oriented programming. Instance happens when we call a class and create an object from it.

If you try to log this line of code, you will get true.

const igor = new Person('Igor', 'basketball')
console.log(igor instanceof Person);

But under the hood, JS is still using prototype inheritance. We are not using classes like classes work in other languages. That is the closest that JavaScript comes to classes.

The idea of Brendan Eich (creator of JS :)) was to make JS different from Java and make a competing language with Java. He didn't have classes in JavaScript back in 1995 :). In JavaScript, we have a principle that everything is an object. So, classes are just objects. In other languages, classes are an actual thing.

Now you can say this is similar to Object.create(). Yes, it is. But as we noted at the beginning of this article, JS is a multi-paradigm. It depends on your team, personal preferences, etc. But most of the code bases that use OOP in JavaScript use this approach with class.

The core aspect of OOP is inheritance, which means passing things down :). So, we are going to see what it looks like in JavaScript.

class Person {
  constructor(name, hobby){
    this.name = name;
    this.hobby = hobby;
  }
  doing(){
    return "I like to " + this.hobby
  }
}

class EuropaPerson extends Person {
  constructor(name, hobby, type){
    this.type = type
  }
}

const igor = new EuropaPerson('Igor', 'basketball', 'slavic')
console.log(igor);

If try to log out new object we are going to get ReferenceError - must call super constructor in derived class before accessing this or returning derived constructor. What this mean!?

We have special keyword called super for super class. It means we need to call super inside of constructor. What does this mean, call super class inside of EuropaPerson class, so super class is Person. EuropaPerson extends Person. If you log it now, everything is working fine. Constructor is working with actual class, and we use super() to extend things.

class EuropaPerson extends Person {
  constructor(type){
    super(name, hobby)
    this.type = type
  }
}

const igor = new EuropaPerson('Igor', 'basketball', 'slavic')
console.log(igor);

Inheritance in JavaScript doesn't copy from an extended class; it simply links to the prototype chain. Unlike other languages, JavaScript is just an object or everything inside JS is called an object. So it is an object inheriting from an object :). There are no technical classes. Other languages, for example, Java or C++, copy objects when we extend classes. There is a bit of efficiency with memory in JS.

Finally new release in ES2022 brings us essential things inside of OOP: private methods and fields, and class fields declarations. Previously, we used something like this to make things private inside of class.

class Employee {
    #name = "Test"; // private field
    setName(name) {
        this.#name = name;
    }
}

In this article, I mentioned inheritance several times. It is one of the few most important pillars of OOP. In object-oriented programming, we have concepts of encapsulation, abstraction and polymorphism. Those are also important in OOP. Encapsulation can be defined as “the packing of data and functions into one component”. Packing, which is also known as bundling, grouping and binding, simply means to bring together data and the methods which operate on data. The component can be a function, a class or an object. Abstraction is a way of hiding the implementation details and showing only the functionality to the users. In other words, it ignores the irrelevant details and shows only the required one. And finally Polymorphism is one of the core concepts of object-oriented programming languages where poly means many and morphism means transforming one form into another. Polymorphism means the same function with different signatures is called many times. I don't go deeper to those concepts, this article is just about main concepts of OOP in JavaScript and the development path how we got here.

In conclusion, despite those new concepts, JavaScript is not well-suited for most of the work we do on the server side, but on the other hand, it can attract backend developers to do something on frontend :).