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
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);
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();
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
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());
};
}
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
You may encounter another version of how to declare a class:
function Product(name, price) {
this.name = name;
this.price = price;
}
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;
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
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;
};
}
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);
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();
//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());
};
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
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS
- http://voidcanvas.com/es6-private-variables/
- https://medium.com/@rajaraodv/is-class-in-es6-the-new-bad-part-6c4e6fe1ee65
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance
- https://en.wikipedia.org/wiki/Object-oriented_programming
Top comments (10)
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:
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.
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.
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.
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.
Nice article!
But it seems you've got a mistake in this code block:
it should be like:
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.Still, it's entirely useless, you should add a getter or something to avoid confusing people :)
True, have added a getter method. Thanks for the input to both of you!
You're right in this case
sry :]
Cool, nice explanation 👍