Object-Oriented Programming in JavaScript
Object-oriented programming is a foundation for many programming languages. So, we'll familiarize ourselves with this paradigm, put it in context and use it in practice.
Last updated: October 13, 2022Lately, I've been writing about specific technologies: React, Gatsby, or Netlify CMS. But, in my first post, I promised universal knowledge. Ten posts later, I think it's time to fulfill the promise. This post will be more theoretical. But I don't want to make it too abstract, so I'll write about OOP in the context of JavaScript language.
OOP is one of the programming paradigms
Even before we touch on the definition of OOP, we need a short history lesson. It will give us context, and a bigger picture, hence a better understanding.
Every code boils down to ones and zeros. It's what computers understand. The lowest-level programming paradigms are machine code, which directly represents the instructions as a sequence of numbers. It's easy to understand for machines but hard for humans.
In the 1960s, there was a development of assembly languages. It is a little step up from machine code. Writing in assembly, we can use logical operators such as READ, WRITE, GET, and PUT or use symbolic labels for memory addresses. I even wrote some assembly code at my university, which looked like this:
NASM
1; hello-DOS.asm - single-segment, 16-bit "hello world" program2;3; assemble with "nasm -f bin -o hi.com hello-DOS.asm"45 org 0x100 ; .com files always start 256 bytes into the segment67 ; int 21h is going to want...89 mov dx, msg ; the address of or message in dx10 mov ah, 9 ; ah=9 - "print string" sub-function11 int 0x21 ; call dos services1213 mov ah, 0x4c ; "terminate program" sub-function14 int 0x21 ; call dos services1516 msg db 'Hello, World!', 0x0d, 0x0a, '$' ; $-terminated message
Even though it's hard to understand and not sophisticated as modern languages, people still use it in things like embedded systems.
C, COBOL, or BASIC are examples of third-generation languages. All these languages follow a procedural paradigm - you write procedures a computer needs to follow to solve a specific problem. Code is easier to write, but still, there is no structure or organization. We have just step-by-step instructions.
People started agreeing on good practices, and after some time, they invented object-oriented programming languages such as C#, Python, or JavaScript. Programmers can now model real-world things with objects. An object is the foundational building block of all these languages.
Also, another paradigm called functional programming is gaining attention. It works well with concurrent and distributed computing - hot topics now. Functions are building blocks in this paradigm. They behave like mathematical functions. Most programming languages support more than one programming paradigm. JavaScript is one of them.
FP and OOP
In all programs, there are two primary components:
- Data
- Behavior
Object-oriented programming says that bringing together the data and its behavior in a single location - object - makes the program easier to understand. It's like packing everything inside a tightly sealed box.
Functional programming says that data and behavior are distinctly different things and should be kept separately for clarity. It is like a pipe with some valves - the data flows from one end to the other, changing along the way.
It's not functional programming vs. object-oriented programming. I used OOP in Node and FP in React. Don't hang on tightly to one. Both have use cases and can be complementary.
Why use OOP?
As I mentioned, we want code that is easy to understand. We read code more often than we write it. Current software can be very complex. Windows 10 have about 50 million lines of code, and Facebook has even more. An average app can still have thousands of lines. To make this complex code more organized, we can use our paradigm. Using OOP, we want the code to be:
- Clear
- Easy to extend
- Easy to maintain
- Memory efficient
- DRY (Don't repeat yourself)
OOP in JavaScript
We've just moved through the short history of programming paradigms featuring different languages. Now, we will focus on one - JavaScript - and write some code to show them in practice. We will start with structural code and move toward more object-oriented code. JavaScript is evolving, and we didn't always have syntax for clear OOP.
Let's say we have two computers: a desktop and a laptop, and we want to model them in code. The first naive and procedural approach can look like this:
Procedural codeJAVASCRIPT
1const desktop = {2 name: 'Mac Studio',3 gpu: 'AMD Radeon Pro W6800X',4 info() {5 console.log(`Name of the computer: ${this.name}`)6 }7}89const laptop = {10 name: 'Macbook Air',11 display: '13.6"',12 info() {13 console.log(`Name of the computer: ${this.name}`)14 }15}
We can create an object for each machine. But imagine we want ten more. We would have to create an object for each one by hand or copy-paste multiple lines of code. We would repeat ourselves constantly.
Factory Functions
To limit repeating, we can create a function that will make objects for us - a factory function.
Factory FunctionJAVASCRIPT
1function createComputer(name) {2 return {3 name,4 info() {5 console.log(`Name of the computer: ${this.name}`)6 }7 }8}910const desktop = createComputer('Mac Studio')11const laptop = createComputer('Macbook Air')
We can call the function multiple times to create multiple objects. We don't repeat ourselves as much. But, there is another problem. Look at the info()
function. It stays the same for every computer, yet we copy it each time. It's not very memory-efficient.
Object.create()
Some time ago, developers added the create()
method on the built-in Object
. It uses an existing object as the prototype for the newly created object. This way, we can define the function once and inherit it through JavaScript prototype inheritance.
Object.create()JAVASCRIPT
1const computerFunctions = {2 info() {3 console.log(`Name of the computer: ${this.name}`)4 }5}67function createComputer(name) {8 let newComputer = Object.create(computerFunctions)9 newComputer.name = name10 return newComputer11}1213const desktop = createComputer('Mac Studio')14const laptop = createComputer('Macbook Air')
JavaScript doesn't have classical inheritance known from languages like Java or C++. It has only one construct - an object. But it's not a bug - it's a feature! Each object has a link to another one called its prototype. These linked objects create a prototype chain.
We could stop here. We don't repeat ourselves, and the code is memory efficient. But, there are other parts of OOP we could improve. Earlier, I was talking about this sealed box in the context of OOP. We don't have that here, and the above snippet is a little hard to understand. Let's use this knowledge about prototypes and see if we can improve something.
Constructor Functions
Any function invoked using the new
keyword is called a constructor function. Built-in objects like Array
or Function
are in fact constructor functions. In JavaScript, there is a constructor function for everything (with an exception for null
and undefined
). This new
keyword does a few things behind the scenes:
- It creates a new object.
- Returns that object.
- Binds
this
to the returned object. - Links object's prototype to the constructor function.
Constructor FunctionJAVASCRIPT
1function Computer(name) {2 this.name = name3}45Computer.prototype.info = function () {6 console.log(`Name of the computer: ${this.name}`)7}89const desktop = new Computer('Mac Studio')10const laptop = new Computer('Macbook Air')
We can use the prototype to define the shared function once, and it will be available for every object created with the constructor function.
Be aware - it won't work with arrow functions. Arrow functions use lexical scope - they define this
based on their place in code. If we define info()
as an arrow function in our snippet, this
will be pointing to the window
.
JavaScript developers have been writing things that way for a long time. You can probably spot similar snippets in older code bases. But, with ES6, we've got a new way to write object-oriented code.
Classes
New syntax was introduced to JavaScript language (alongside many other features) with ECMAScript 6 specification. The below snippet may look familiar to developers who have used C++ or other object-oriented languages.
ClassesJAVASCRIPT
1class Computer {2 constructor(name) {3 this.name = name4 }5 info() {6 console.log(`Name of the computer: ${this.name}`)7 }8}910const desktop = new Computer('Mac Studio')11const laptop = new Computer('Macbook Air')
The best part is that everything is in one "box." All properties and methods are between curly braces. We don't need to use prototypes explicitly.
But remember, even though there is a class
keyword, it is syntactic sugar. Underneath the hood, we're still using prototype inheritance. In other languages, classes are an actual thing. Opposed to that, in JavaScript, they are still objects.
You may argue that using the class
keyword with prototype inheritance is deceptive. And it is a little. But I think the benefit of readability overweight the cons. In my opinion, this code is easy to understand and looks like OOP. Especially when we add inheritance.
Classes and inheritanceJAVASCRIPT
1class Computer {2 constructor(name) {3 this.name = name4 }5 info() {6 console.log(`Name of the computer: ${this.name}`)7 }8}910class Desktop extends Computer {11 constructor(name, gpu) {12 super(name)13 this.gpu = gpu14 }15}1617class Laptop extends Computer {18 constructor(name, display) {19 super(name)20 this.display = display21 }22}2324const desktop = new Desktop('Mac Studio', 'AMD Radeon Pro W6800X')25const laptop = new Laptop('Macbook Air', '13.6"')
Desktop
and Laptop
have prototype chains up to the Computer
class. Subclasses inherit from the base class (also called superclass). To inherit properties, we must invoke the super()
method inside the constructor. Under this method, we can easily add properties specific to the subclass.
This type of inheritance doesn't copy everything from the base class like in classical object-oriented languages. Instead, it links the prototype chain with the extends
keyword. So, it has memory-saving benefits.
Four pillars of OOP
Talking about pillars at the end of the post may look strange. But it is a good summary, and we've just learned them!
Encapsulation
Look at our classes - everything is inside objects wrapped by curly braces. Our code consists of closed units. We've encapsulated our code. The code written this way is easy to understand.
Abstraction
Our examples are uncomplicated to simplify reasoning. But imagine the info()
method is complex. You don't need to know how it works to use it. The complexity is abstracted away from you. Abstraction means hiding complexity from the user. The code is easy to use.
Inheritance
Our classes inherit from each other parts of the functionality. We avoid rewriting and repeating ourselves. We also save space by having shared methods.
Polymorphism
Ok, I lied. We haven't touched on one of the pillars. But I'm redeeming myself now. The word "polymorphism" comes from the Greek word "polymorphos," which means "many forms." The etymology accurately describes what we want to accomplish with polymorphism in OOP. I don't know if there is one correct definition of polymorphism. I don't want to argue with people smarter than me, so I'm not claiming I'm giving a precise definition. But the idea is that we can call the same method on different objects, and each object responds distinctly. JavaScript is a dynamically typed language, so it limits polymorphism. But we can write something like this:
Classes and polymorphismJAVASCRIPT
1class Computer {2 constructor(name) {3 this.name = name4 }5 info() {6 console.log(`Name of the computer: ${this.name}`)7 }8}910class Desktop extends Computer {11 constructor(name, gpu) {12 super(name)13 this.gpu = gpu14 }15 info() {16 console.log(`Name of the computer: ${this.name}\nGPU: ${this.gpu}`)17 }18}1920class Laptop extends Computer {21 constructor(name, display) {22 super(name)23 this.display = display24 }25 info() {26 console.log(`Name of the computer: ${this.name}\nDisplay: ${this.display}`)27 }28}2930const desktop = new Desktop('Mac Studio', 'AMD Radeon Pro W6800X')31const laptop = new Laptop('Macbook Air', '13.6"')3233desktop.info()34//Name of the computer: Mac Studio35//GPU: AMD Readeon Pro W6800X36laptop.info()37//Name of the computer: Macbook Air38//Display: 13.6"
Our info()
method is polymorphic. Depending on the object, it displays info about different computer components.
Summary
We've explored object-oriented programming principles, concepts, and goals. Also, now we know how to implement them in future JavaScript projects. I hope this post was helpful and you've got to the end. And if you don't have enough, check out below links: