DEV Community

Pepe Benitez
Pepe Benitez

Posted on

Object Oriented Programming in Javascript πŸ€“

Hi! Programming can be overwhelming 😫 but once you are comfortable with some basic concepts, it starts to feel like a superpower πŸ¦Έβ€β™€οΈ and Javascript is one of the coolest languages to learn! πŸ’―

In this document you can find a summary of Object Oriented Programming in Javascript. We will cover:

  • What is Object Oriented Programming?
  • OOP in Javascript
  • Classes
  • Prototype Model
  • Guiding Principles

What is Object Oriented Programming?

When we talk about Object Oriented Programming or OOP, we are really talking about how we organize our code. Many times, deciding where we put our code is not straight forward or easy. OOP gives us a paradigm to start working with.

The main point is to model things from real life using objects, which can contain data or properties, and can execute actions or methods. For example, a person can have many attributes such as name, age or nationality, and it can perform several actions such as running or eating.

If we want to create a lot of different people in our code, things can get out of hand very quickly.

OOP has several concepts that help us maintain our code manageable. We do no want Spaghetti Code 🍝 Some of them are:

  • Abstraction βˆ’Β We provide only essential information to the outside world while hiding their background details. As you build more and more complicated objects, abstractions helps us simplify things.
  • Encapsulation βˆ’Β Refers to storing all of our data and functionality into a single unit. It is also a way of restricting access to certain properties.
  • Inheritance βˆ’Β It is the ability to give data and functionality to an object based on another object. It helps us write a lot less code and promotes reusability.

OOP in Javascript

Javascript is a very flexible language, and you can use different paradigms to build your code. There are also a lot of strong opinions on the subject of using OOP with javascript, so take this article not as a definitive guide of how to do things, but rather a guide on what OOP is and how you can you use it in your advantage.

Different languages have different ways of doing things, and there is not necessarily a better way of doing things. In Javascript, you have several options to take advantage of the uses of OOP.

Objects is Javascript are like objects in real life, with things they have (properties) and things they do (methods).

You might have heard that "Everything is JS is an object". Although not entirely true, even things that are not objects, such as primitive types, can behave like objects sometimes because javascript wraps them into objects in the background.

let name = 'string' // primitive type

let name2 = new String('String object') // objects
Enter fullscreen mode Exit fullscreen mode

The idea at the hearth of object oriented programming is that we are not limited to built-in objects. We can make our own.

Object literal notation

We can build our own objects using the object literal notation, with whatever properties and methods we want.

let userOne = {
    email: 'some@email.com',
    name: 'Pepe',
    login() {
        console.log(this.email, 'has logged in')
    }
}
Enter fullscreen mode Exit fullscreen mode

When the this keyword is inside of an object, this refers to the object itself.

We can use dot nation to change data from our objects.

userOne.name = 'A name'
Enter fullscreen mode Exit fullscreen mode

We can also user bracket notation.

userOne['name'] = 'Another name'
Enter fullscreen mode Exit fullscreen mode

We can also create or define new properties or methods using dot or bracket notation, even if they don't already exist.

userOne.age = 28
Enter fullscreen mode Exit fullscreen mode

However, we can start noticing some problems. It is probably better to put everything in the object literal notation, instead of adding new properties later in the code, so everything is encapsulated.

Also, if we want to create several object instances of user, we would need to repeat a lot of our code and work hard to change all users if we decide that the properties and methods of users need to change instead of them inheriting from one single source of truth. This is where we can tap into javascript functionality such as classes, constructors and prototypes to help us.

Prototype model vs Classes

There are not real classes built into javascript. Everything is based on the Prototype Model under the hood, but with ES6 we can emulate the idea of classes to make things a bit easier to understand. This is what we call syntactic sugar. You can use either, and we will explain how classes and the prototype model work.

Classes

Classes are blueprints of an object, but they are not specific. We identify and define that an object will have specific properties, but we don't specify their values.

The cool things in that when we create instances, they will already have all the basic functionality built-in, and we can pass values dynamically through parameters.

To use classes we define them using the keyword class and the name of the class. By convention, we capitalize and CamelCase class names. Inside we put all data and functionality (properties and methods).

class User {
}
Enter fullscreen mode Exit fullscreen mode

Constructor

Although you may see similarities between class and object syntax, there is one important method that sets them apart. It’s called the constructor method. JavaScript calls the constructor() method every time it creates a new instance of a class.

An instance is an object that contains the property names and methods of a class, but with unique property values.

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
Enter fullscreen mode Exit fullscreen mode

Inside of the constructor() method, we use the this keyword. In the context of a class, this refers to an instance of that class.

We don't want to hard-code the data, we want to pass it as arguments.

To create a new instance we use the new keyword.

class User {
    constructor(name, email){
        this.name = name;
        this.email = email;
    }
}

let userOne = new User('el@pepebenitez.dev', 'Pepe')
Enter fullscreen mode Exit fullscreen mode

The new keyword does 3 things:

  • Creates an empty object { }
  • Sets the value of this to the new empty object
  • Calls the constructor method, runs the code inside of it, and then returns the new instance

Class methods

To use methods, we don't define them inside of the constructor, we do it outside of it. You cannot use commas between the methods.

We have access to the this keyword inside of methods as well.

class User {
    constructor(name, email){
        this.name = name;
        this.email = email;
    }
    logIn(){
        console.log(this.email, 'just logged in')
    }
    logOut(){
        console.log(this.email, 'just logged out')
    }
}

let userOne = new User('el@pepebenitez.dev', 'Pepe')

userOne.logIn()
// el@pepebenitez.dev just logged in
Enter fullscreen mode Exit fullscreen mode

The syntax for calling methods and getters on an instance is the same as calling them on an object β€” append the instance with a period, then the property or method name. For methods, you must also include opening and closing parentheses.

Class inheritance

When multiple classes share properties or methods, they become candidates for inheritance β€” a tool we can use to decrease the amount of code we need to write.

With inheritance, you can create a parent class (also known as a superclass) with properties and methods that multiple child classes (also known as subclasses) share. The child classes inherit the properties and methods from their parent class, by extending them from the parent.

When we call extends in a class declaration, all of the parent methods are available to the child class. In addition to the inherited features, child classes can contain their own properties, getters, setters, and methods.

With inheritance we can pass al the basic functionality easily into other classes

class Cat extends Animal {
  constructor(name, usesLitter) {
    super(name);
    this.usesLitter = usesLitter;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s pay special attention to our new keywords: extends and super.

The extends keyword makes the methods of the parent class available inside the child class.

The super keyword calls the constructor of the parent class. In this case, super(name) passes the name argument of the Cat class to the constructor of the Animal class.

usesLitter is a new property that is unique to the Cat class, so we set it in the Cat constructor.

IMPORTANT In a constructor(), you must always call the super method before you can use the this keyword β€” if you do not, JavaScript will throw a reference error. To avoid reference errors, it is best practice to call super on the first line of subclass constructors.

You can find more resources on Javascript classes here:

Class basic syntax

Classes - JavaScript | MDN

JavaScript Classes

Prototype Model

Remember that we are just using pretend classes. The javascript prototype model was the original way to create or emulate using classes before the class keyword, and under the hood javascript is still using the prototype model.

Some people don't like the use of classes in Javascript.

Is "Class" In ES6 The New "Bad" Part?

So knowing how the prototype model works will help us become better developers, have more flexibility and be more efficient at debugging.

Constructor Functions

We still need to use a constructor function. A constructor function is what creates objects.

function User() {}

let userOne = new User()
Enter fullscreen mode Exit fullscreen mode

We can still use the new keyword. And it is still doing the same things (creating a new object, binding the context of the this keyword equal to that object, and passes it to the constructor function). We also have access to the this keyword.

function User() {
    this.name = '';
    this.email = '';
    this.online = false;
    this.logIn = function() {
        console.log(this.email, 'has logged in')
    };
}

let userOne = new User()
Enter fullscreen mode Exit fullscreen mode

However, we don't usually put our methods inside of the constructor function. That is when we use the prototype property.

Prototype property

All objects have a prototype property. A prototype is like a map for the object type and its methods. We use the prototype property to store and define our methods.

function User() {
    this.name = '';
    this.email = '';
    this.online = false;
}

User.prototype.logIn = function(){
    this.online = true;
    console.log(this.email, 'has logged in')
}

let userOne = new User()
Enter fullscreen mode Exit fullscreen mode

This is all going on in the background when we use class.

Prototype inheritance

We can also use inheritance with prototype.

function Admin(...args){ // here we are using a rest parameter
    console.log(args)
}

let admin = new Admin('admin name', 'admin@email.com')
Enter fullscreen mode Exit fullscreen mode

We want to call the User constructor method from the Admin constructor method, so that it inherits the basic functionality.

function User() {
    this.name = '';
    this.email = '';
    this.online = false;
}

function Admin(...args){ // here we are using a rest parameter
    User.apply() // takes the user constructor function and runs it
}

let admin = new Admin('admin name', 'admin@email.com')
Enter fullscreen mode Exit fullscreen mode

In the apply method we want to pass two parameters:

  • What is the context of this. Sounds weird but we just need to pass this because we are calling it inside of a constructor function, so this in that context refers to the instance to which we are applying the new inherited properties and methods.
  • The actual arguments we are receiving in an array format.
function User() {
    ...
}

function Admin(...args){ // here we are using a rest parameter
    User.apply(this, args) // takes the user constructor function and runs it
    this.role = 'Administrator'
}

let admin = new Admin('admin name', 'admin@email.com')
Enter fullscreen mode Exit fullscreen mode

If we want to inherit methods, we use the prototype property of the child object.

Admin.prototype = Object.create(User.prototype)
Enter fullscreen mode Exit fullscreen mode

You can see that using classes can make some thins a lot simpler and easier, but it is still very good to understand what is actually going on.

Guiding principles

When we are talking about OOP, it is difficult not to mention the SOLID principles. There principles are meant to guide us when we are designing our code structure. Two good goals to have in mind is for everything to have only one responsibility and that everything should work individually as we want them to, without relying on other objects:

Single Responsibility Principle

One of the most important things to remember is the Single Responsibility Principle which states that a class (or object) should only have one responsibility. This doesn’t mean that an object can only do one thing, but it does mean that everything an object does should be part of one responsibility.

Loosely Coupled Objects

Obviously, all of our objects are intended to work together to form our final application. You should take care, however, to make sure that your individual objects can stand alone as much as possible. Tightly coupled objects are objects that rely so heavily on each other that removing or changing one will mean that you have to completely change another one.

You can read more about the SOLID principles here:

5 Principles that will make you a SOLID JavaScript Developer

S.O.L.I.D The first 5 principles of Object Oriented Design with JavaScript

Bonus

Static Methods

Sometimes you will want a class to have methods that aren’t available in individual instances, but that you can call directly from the class You can use the static keyword to define static methods that can only be called from the class and not from the instance.

class Animal {
  constructor(name) {
    this._name = name;
    this._behavior = 0;
  }

  static generateName() {
    const names = ['Angel', 'Spike', 'Buffy', 'Willow', 'Tara'];
    const randomNumber = Math.floor(Math.random()*5);
    return names[randomNumber];
  }
}
Enter fullscreen mode Exit fullscreen mode

Because of the static keyword, we can only access .generateName() by appending it to the Animal class.

console.log(Animal.generateName()); // returns a name
const tyson = new Animal('Tyson'); 
tyson.generateName(); // TypeError
Enter fullscreen mode Exit fullscreen mode

Method chaining

A little trick to call several methods with less code.

userOne.logIn().logOut()
Enter fullscreen mode Exit fullscreen mode

In order to do this we need to return the instance of the object. The instance is stored in the keyword this. We just need to return this at the end of every method.

class User {
    constructor(name, email){
        this.name = name;
        this.email = email;
    }
    logIn(){
        console.log(this.email, 'just logged in')
        return this
    }
    logOut(){
        console.log(this.email, 'just logged out')
        return this
    }
}
Enter fullscreen mode Exit fullscreen mode

Useful resources on Javascript

JavaScript | MDN

freeCodeCamp.org

JavaScript Tutorial: Learn JavaScript For Free | Codecademy

JavaScript Code to go


Hi! My name is Pepe πŸ‘Ύ, and I am from Panama in Central America 🌴🌞🌴 You can find me in linkedin, twitter or github.

  • If you found this useful feel free to share it!
  • If you have any questions, recommendations or general comments feel free to drop me a message!

Top comments (0)