DEV Community

Cover image for JavaScript and Object-Oriented Programming
Rainer Hahnekamp
Rainer Hahnekamp

Posted on • Originally published at rainerhahnekamp.com

JavaScript and Object-Oriented Programming

This article is written for students of JavaScript that don’t have any prior knowledge in object-oriented programming (OOP). I focus on the parts of OOP that are only relevant for JavaScript and not OOP in general. Therefore, I skip polymorphism because I think it fits better with a static-typed language.

Why do you need to know this?

Have you picked JavaScript to be your first programming language? Do you want to be a hot-shot developer who works on giant enterprise systems spanning a hundred-thousand lines of code or more?

Unless you learn to fully embrace Object-Oriented Programming, you will be well and truly lost.

Different Mindsets

In football, you can play from a safe defense, you can play with high balls from the sides or you can attack like there is no tomorrow. All of these strategies have the same objective: To win the game.

The same is true for programming paradigms. There are different ways to approach a problem and design a solution.

Object-oriented programming, or OOP, is THE paradigm for modern application development and is supported by major languages like Java, C# or JavaScript.

The Object-Oriented Paradigm

From the OOP perspective, an application is a collection of “objects” that communicate with each other. We base these objects on things in the real world, like products in inventory or employee records. Objects contain data and perform some logic based on their data. As a result, OOP code is very easy to understand. What is not so easy is deciding how to break an application into these small objects in the first place.

If you are like me when I heard it the first time, you have no clue what this actually means — it all sounds very abstract. Feeling that way is absolutely fine. It’s more important that you’ve heard the idea, remember it, and try to apply OOP in your code. Over time, you will gain experience and align more of your code with this theoretical concept.

Lesson: OOP based on real-world objects lets anyone read your code and understand what’s going on.

Object as Centerpiece

Object as Centerpiece
A simple example will help you see how JavaScript implements the fundamental principles of OOP. Consider a shopping use case in which you put products into your basket and then calculate the total price you must pay. If you take your JavaScript knowledge and code the use case without OOP, it would look like this:

const bread = {name: 'Bread', price: 1};
const water = {name: 'Water', price: 0.25};

const basket = [];
basket.push(bread);
basket.push(bread);
basket.push(water);
basket.push(water);
basket.push(water);

const total = basket
  .map(product => product.price)
  .reduce((a, b) => a + b, 0);

console.log('one has to pay in total: ' + total);
Enter fullscreen mode Exit fullscreen mode

The OOP perspective makes writing better code easier because we think of objects as we would encounter them in the real world. As our use case contains a basket of products, we already have two kinds of objects — the basket object and the product objects.

The OOP version of the shopping use case could be written like:

const bread = new Product("bread", 1);
const water = new Product("water", .25)

const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.printShoppingInfo();
Enter fullscreen mode Exit fullscreen mode

As you can see in the first line, we create a new object by using the keyword new followed by the name of what’s called a class (described below). This returns an object that we store to the variable bread. We repeat that for the variable water and take a similar path to create a variable basket. After you have added these products to your basket, you finally print out the total amount you have to pay.

The difference between the two code snippets is obvious. The OOP version almost reads like real English sentences and you can easily tell what’s going on.

Lesson: An object modeled on real-world things consists of data and functions.

Class as Template

Class as Template
We use classes in OOP as templates for creating objects. An object is an “instance of a class” and “instantiation” is the creation of an object based on a class. The code is defined in the class but can’t execute unless it is in a live object.

You can look at classes like the blueprints for a car. They define the car’s properties like torque and horsepower, internal functions such as air-to-fuel ratios and publicly accessible methods like the ignition. It is only when a factory instantiates the car, however, that you can turn the key and drive.

In our use case, we use the Product class to instantiate two objects, bread and water. Of course, those objects need code which you have to provide in the classes. It goes like this:

function Product(_name, _price) {
  const name = _name;
  const price = _price;

  this.getName = function() {
    return name;
  };

  this.getPrice = function() {
    return price;
  };
}

function Basket() {
  const products = [];

  this.addProduct = function(amount, product) {
    products.push(...Array(amount).fill(product));
  };

  this.calcTotal = function() {
    return products
      .map(product => product.getPrice())
      .reduce((a, b) => a + b, 0);
  };

  this.printShoppingInfo = function() {
    console.log('one has to pay in total: ' + this.calcTotal());
  };
}
Enter fullscreen mode Exit fullscreen mode

A class in JavaScript looks like a function, but you use it differently. The name of the function is the class’s name and is capitalised. Since it doesn’t return anything, we don’t call the function in the usual way like const basket = Product("bread", 1);. Instead, we add the keyword new like const basket = new Product("bread", 1);.

The code inside the function is the constructor and is executed each time an object is instantiated. Product has the parameters _name and _price. Each new object stores these values inside of it.

Furthermore, we can define functions that the object will provide. We define these function by prepeding the this keyword which makes them accessible from the outside (see Encapsulation). Notice that the functions have full access to the properties.

Class Basket doesn’t require any arguments to create a new object. Instantiating a new Basket object simply generates an empty list of products that the program can fill afterwards.

Lesson: A class is a template for generating objects during runtime.

Encapsulation

Encapsulation
You may encounter another version of how to declare a class:

function Product(name, price) {
  this.name = name;
  this.price = price;
}
Enter fullscreen mode Exit fullscreen mode

Mind the assignment of the properties to the variable this. At first sight, it seems to be a better version because it doesn't require the getter (getName & getPrice) methods anymore and is therefore shorter.

Unfortunately, you have now given full access to the properties from the outside. So everybody could access and modify it:

const bread = new Product('bread', 1)
bread.price = -10;
Enter fullscreen mode Exit fullscreen mode

This is something you don't want as it makes the application more difficult to maintain. What would happen if you added validation code to prevent, for example, prices less than zero? Any code that accesses the price property directly would bypass the validation. This could introduce errors that would be difficult to trace. Code that uses the object’s getter methods, on the other hand, are guaranteed to go through the object’s price validation.

Objects should have exclusive control over their data. In other words, the objects “encapsulate” their data and prevent other objects from accessing the data directly. The only way to access the data is indirectly via the functions written into the objects.

Data and processing (aka. logic) belong together. This is especially true when it comes to larger applications where it is very important that processing data is restricted to specifically-defined places.

Done right, the result OOP produces modularity by design, the holy grail in software development. It keeps away the feared spaghetti-code where everything is tightly coupled and you don’t know what happens when you change a small piece of code.

In our case, objects of class Product don’t let you change the price or the name after their initialisation. The instances of Product are read-only.

Lesson: Encapsulation prevents access to data except through the object’s functions.

Inheritance

Inheritancs
Inheritance lets you create a new class by extending an existing class with additional properties and functions. The new class “inherits” all of the features of its parent, avoiding the creation of new code from scratch. Furthermore, any changes made to the parent class will automatically be available to the child class, making updates much easier.

Let’s say we have a new class called Book that has a name, a price and an author. With inheritance, you can say that a Book is the same as a Product but with the additional author property. We say that Product is the superclass of Book and Book is a subclass of Product:

function Book(_name, _price, _author) {
  Product.call(this, _name, _price);
  const author = _author;

  this.getAuthor = function() {
    return author;
  };

}
Enter fullscreen mode Exit fullscreen mode

Note the additional Product.call along the this as first argument. Please be aware: Although book provides the getter methods, it still doesn’t have direct access to the properties name and price. Book must call that data from the Product class.
You can now add a book object to the basket without any issues:

const faust = new Book('faust', 12.5, 'Goethe');
basket.addProduct(1, faust);
Enter fullscreen mode Exit fullscreen mode

Basket expects an object of type Product and, since book inherits from Product through Book, it is also a Product.

Lesson: Subclasses can inherit properties and functions from superclasses while adding properties and functions of their own.

JavaScript and OOP

You will find three different programming paradigms used to create JavaScript applications. They are Prototype-Based Programming, Object-Oriented Programming and Functional-Oriented Programming.

The reason for this lies in JavaScript’s history. Originally, it was prototype-based. JavaScript was not intended as a language for large applications.

Against the plan of its founders, developers increasingly used JavaScript for bigger applications. OOP was grafted on top of the original prototype-based technique.

The prototype-based approach is shown below and is seen as the "classical and default way" to construct classes. Unfortunately it does not support encapsulation.

Even though JavaScript’s support for OOP is not at the same level as other languages like Java, it is still evolving. The release of version ES6 added a dedicated class keyword we could use. Internally, it serves the same purpose as the prototype property, but it reduces the size of the code. However, ES6 classes still lack private properties, which is why I stuck to the “old way”.

For the sake of completeness, this is how we would write the Product, Basket and Book with ES6 classes and also with the prototype (classical and default) approach. Please note that these versions don't provide encapsulation:

// ES6 version

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
}

class Book extends Product {
  constructor(name, price, author) {
    super(name, price);
    this.author = author;
  }
}

class Basket {
  constructor() {
    this.products = [];
  }

  addProduct(amount, product) {
    this.products.push(...Array(amount).fill(product));
  }

  calcTotal() {
    return this.products
      .map(product => product.price)
      .reduce((a, b) => a + b, 0);
  }

  printShoppingInfo() {
    console.log('one has to pay in total: ' + this.calcTotal());
  }
}

const bread = new Product('bread', 1);
const water = new Product('water', 0.25);
const faust = new Book('faust', 12.5, 'Goethe');

const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.addProduct(1, faust);
basket.printShoppingInfo();
Enter fullscreen mode Exit fullscreen mode
//Prototype version

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Book(name, price, author) {
  Product.call(this, name, price);
  this.author = author;
}
Book.prototype = Object.create(Product.prototype);
Book.prototype.constructor = Book;

function Basket() {
  this.products = [];
}
Basket.prototype.addProduct = function(amount, product) {
  this.products.push(...Array(amount).fill(product));
};
Basket.prototype.calcTotal = function() {
  return this.products
    .map(product => product.price)
    .reduce((a, b) => a + b, 0);
};
Basket.prototype.printShoppingInfo = function() {
  console.log('one has to pay in total: ' + this.calcTotal());
};
Enter fullscreen mode Exit fullscreen mode

Lesson: OOP was added to JavaScript later in its development.

Summary

As a new programmer learning JavaScript, it will take time to fully appreciate Object-Oriented Programming. The important things to understand at this early stage are the principles the OOP paradigm is based on and the benefits they provide:

  • Objects modeled on real-world things are the centerpiece of any OOP-based application.
  • Encapsulation protects data from uncontrolled access.
  • Objects have functions that operate on the data the objects contain.
  • Classes are the templates used to instantiate objects.
  • Inheritance is a powerful tool for avoiding redundancy.
  • OOP is more verbose but easier to read than other coding paradigms.
  • Since OOP came later in JavaScript’s development, you may come across older code that uses prototype or functional programming techniques.

Further reading

Top comments (10)

Collapse
 
pat_metzdorf profile image
Patrick Metzdorf • Edited

Unless you learn to fully embrace Object-Oriented Programming, you will be well and truly lost.

Oh c'mon. Most modern Javascript devs are starting to embrace the FP approach and never write a single new in any of their projects (unless some external library forces them to).

Absolutely anything and everything can be achieved in Javascript, and easily, without creating new prototypes or even - god forbid - classes.

But even if you really want to instantiate a common object pattern and just love your instances, you can replace most "classes" with simple factory patterns and keep the beauty of simple functions:

const doStuff = foo => {
  const bar = 'Yea, man!'
  return {
    x: foo.x,
    y: bar,
    sum: foo.x + foo.y,
  }
}

const a = doStuff({x: 1, y: 2})
const b = doStuff({x: 3, y: 4})

etc.

There is literally no reason to use OOP in Javascript, except if you come from a different language and just want to keep using OOP.

Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp

Hi Patrick, I've tried out FP with Clojure in larger applications. It turned out that with growing application size it was getting more and more harder to maintain it.

Further more, I see that many JavaScript devs are using TypeScript so that they can have a better support for OOP (and typings of course).

Quite possible, that we weren't simply not able to apply FP property, but in my experience OOP beats it at a certain application size.

Collapse
 
lucretius profile image
Robert Lippens

Why not have both? I really enjoy the type system provided by TypeScript (which can support both OOP and FP styles) and have begun to use it in all JavaScript projects that aren't one-off scripts. It does make reasoning about one's code much easier.

You will still run into situations where you are better served without classes and you can reach for FP in those scenarios. Anytime I find myself needing to make a class with only static methods for example, I end up just writing a module composed of simple FP-style functions. Anything that requires state I use classes and OOP for. And even within OOP, you can take the knowledge of FP and try to limit the data you mutate.

Thread Thread
 
pat_metzdorf profile image
Patrick Metzdorf • Edited

I'm all for Typescript, especially at scale for the typings, but that has nothing to do with using OOP. The beauty of Typescript is that it's completely opt-in. You can use as much of it, or as little, as you like, depending on what suits your conventions and preferences.

Granted, if you want to go the OOP route, TS will enhance your JavaScript, just as it would for the FP route, depending on how you use it.

Typical FP concepts such as mapping, reducing, currying etc., are all greatly improved by statically defining the types of parameters and return values, for example.

If somebody really likes to embrace the full OOP world, JavaScript isn't ideal. I'd recommend Java, or better yet: Kotlin for that approach.

Collapse
 
oxyyyyy profile image
Alexandr Vlasenko

Nice article!
But it seems you've got a mistake in this code block:

function Book(_name, _price, _author) {
  Product.call(this, _name, _price);
  const author = _author; // <- here
}

it should be like:

function Book(_name, _price, _author) {
  Product.call(this, _name, _price);
  this.author = _author; // <- here
}
Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp

Hi Alexandr, thanks for mentioning but I did this on purpose to enforce encapsulation. So I don't want that anybody can access the author property except the object itself.

I am using this in prototype-based version at the end of this article and mentioned the problem with encapsulation.

Collapse
 
avalander profile image
Avalander

Still, it's entirely useless, you should add a getter or something to avoid confusing people :)

Thread Thread
 
rainerhahnekamp profile image
Rainer Hahnekamp

True, have added a getter method. Thanks for the input to both of you!

Collapse
 
oxyyyyy profile image
Alexandr Vlasenko

You're right in this case
sry :]

Collapse
 
kleene1 profile image
kleene1

Cool, nice explanation 👍