This article was originally written by Adebayo Adams on the Honeybadger Developer Blog.
Much of what you do when writing software is creating and connecting multiple values and methods that work together to provide your application's functionality. Object-oriented programming helps you achieve and implement this much more easily and declaratively.
In this article, you will learn everything you need to know to start using classes and object-oriented programming approaches in JavaScript.
Let's examine what you need to get the most out of this article.
Prerequisites
To follow this tutorial, you need to have basic knowledge of JavaScript.
Object-Oriented Programming
Object-oriented programming (OOP) is a programming paradigm used by developers to structure software applications into reusable pieces of code and blueprints using objects. OOP is used daily in large, complex, and actively maintained projects.
OOP makes it easier to create and manage many parts of your application in isolation and connect them without making them depend on one another. Next, let's look at the four major concepts of OOP.
Abstraction
Abstraction in OOP is the process of exposing only the necessary functions to the user while hiding the complex inner workings to make the programs easier to use and understand.
For example, when you send a message on your phone, all the functions and logic that routes your message to the other person are hidden because you don't need to know them or how they work.
Similarly, in programming, if you are building an API that helps financial apps verify users identity and banking information, the developers that use your API don't need to know which database system you use or how you make calls to your database. All they need to know is the function they need to call and what parameters they need to provide.
Abstraction helps to reduce complexity, increases usability, and makes changes to the application less disruptive.
Encapsulation
Encapsulation is the process of bundling related code into a single unit. Encapsulation makes it impossible for other parts of the code to manipulate or change how the bundled part of the application works unless you explicitly go into that unit and change them.
For example, if you are building a flight-booking API, it makes sense to separate the code that searches for the flight from the code that books the flight. This way, two different developers can work on each part seamlessly without conflicts because each developer will have no reason to manipulate the other's code directly.
Encapsulation helps you reduce complexity and increases code reusability.
Inheritance
Inheritance in OOP reduces code duplication, enabling you to build a part of your application on another by inheriting properties and methods from that part of the application.
For example, when building an e-commerce delivery app with multiple types of vehicles, the Car
and Motorcycle
classes can inherit pickUp
and dropOff
functions from the Vehicle
class. Classes will be explained in more detail later in the article.
A significant advantage of inheritance in OOP is the reduction of code duplication.
Polymorphism
In programming, polymorphism is a term used to describe a code or program that can handle many types of data by returning a response or result based on the given data.
For example, you have a form that is used to add products to a product catalog and three different types of products. With polymorphism, you can create a single class method to format all kinds of products before adding them to the database.
Polymorphism helps you eliminate complex and unnecessary if
and switch
statements, as they can become lengthy when writing complex programs.
Let's look at JavaScript objects in the next section.
JavaScript Objects
An object in JavaScript is an unordered collection of key-value pairs, also known as properties and values. Object keys can be a string value, while the values can be any type. Here are some examples:
- String
- Number
- Boolean
- Array
- Function
Next, let's look at how to create objects in JavaScript.
Creating Objects
Creating an object in JavaScript is fairly easy:
const car = {
name: 'Ford',
year: 2015,
color: 'red',
description: function () {
return `${this.name} - ${this.year} - ${this.color}`;
}
}
The code above declares a car
object with name
, year
, color
, and a description
function as its properties.
Accessing Object Properties
There are two ways to access an object property in JavaScript; let's look at them below:
Using the Dot Notation
The following example shows how to access object properties using dot notation.
const country = {
name: 'Spain',
population: 4000000,
description: function () {
return `${this.name} - ${this.population}`;
}
}
If you have an object like the one shown above, you can use the format objectName.keyName
, which should return the value of the given key:
console.log(country.name); // returns 'Spain'
Using the Array Notation
The following example shows how to access object properties using the array notation.
const job = {
role: "Software Engineer",
'salary': 200000,
applicationLink: "meta.com/careers/SWE-role/apply",
isRemote: true,
};
If you have an object like the one above, you can use the format objectName[keyName],
which should return the value of the given key:
console.log(job[role]); // returns 'Software Engineer'
Additionally, you can only access the salary
property using the array notation. Trying to get it with the dot notation will return an error:
console.log(job.'salary'); // SyntaxError: Unexpected string
Next, let's see how to modify object properties.
Modifying Object Properties
You can dynamically add, edit, and delete object properties in JavaScript.
Editing Properties
You can use the assignment =
operator to modify object values. Here is an example:
const person = {
name: "John",
age: 30,
job: "Software Developer",
country: "Nigeria",
car: "Ford",
description: function () {
return `${this.name} - ${this.age} - ${this.job.role} - ${this.country.name} - ${this.car.name}`;
},
};
You can also change the value of name
in the above object:
person.name = "Smith Doe";
console.log(person.name); // returns "Smith Doe"
Adding New Properties
One of the significant differences between objects in other languages and objects in JavaScript is the ability to add a new property to an object after creation.
To add a new property to an object, you use the dot notation:
// adding a new `race` property
person.race = "Asian";
console.log(person.race); // returns "Asian"
The code above adds a new race
property with the value "Asian".
Deleting Object Properties
JavaScript allows you to delete properties from an object by using the delete
keyword:
delete person.race;
console.log(person.race); // returns 'undefined'
The code above deletes the race
property, and accessing the race
property will return undefined.
Note: You can only delete existing object properties.
Checking Properties
Before adding to or deleting properties from an object, it is an excellent idea to determine whether the property exists on the object. This seemingly simple check will save you hours of debugging a bug caused by duplicate values.
To determine whether a property exists on an object, you can use the in
keyword:
console.log('name' in person) // returns true
console.log('race' in person) // returns false
The code above returns true
for the name check because the name exists and false
for the deleted race
property.
Now that you know what objects are and how to use them, let's take the next step to OOP in JavaScript by learning about classes.
Classes
In programming, a class is a structure defined by a programmer that is then used to create multiple objects of the same type. For example, if you are building an application that handles various cars, you can create a Car
class that has the essential functions and properties that apply to all vehicles. Then, you can use this class to make other cars and add properties and methods that are specific to each vehicle to them.
To extend the objects you saw in the previous example, if you wanted to create another job object, you would need to create something like this:
const job2 = {
role: "Head of Design",
salary: 175000,
applicationLink: "amazon.com/careers/hod-role",
isRemote: false,
};
However, as you can see, the above way of creating multiple objects is error-prone and not scalable because you can't make 100 job objects by writing that out every time you need to create a job. This is where classes come in handy.
Creating Classes
You can create a Job
class to simplify creating multiple jobs:
class Job {
constructor(role, salary, applicationLink, isRemote) {
this.role = role;
this.salary = salary;
this.applicationLink = applicationLink;
this.isRemote = isRemote;
}
}
The code above creates a Job
class with role
, salary
, applicationLink
, and isRemote
properties. Now you can create different jobs using the new
keyword:
let job1 = new Job(
"Software Engineer",
200000,
"meta.com/careers/SWE-role/apply",
true
);
let job2 = new Job(
"Head of Design",
175000,
"amazon.com/careers/hod-role",
false
);
The code above creates two different jobs with all the required fields. Let's see if this works by printing both jobs out in the console:
console.log(job1);
console.log(job2);
The above code should print out both jobs in the console:
The image shows both jobs and all their properties in the console.
The this
Keyword
The this
keyword is considered the most confusing keyword in JavaScript because it means different things depending on where it appears in your code.
In the above example, the this
keyword refers to any object created with the Job
class. Therefore, inside the constructor
method, this.role = role;
means "define the role
property of this object you're creating as any value given to the constructor".
Also, note that the initial values you give must be in order when creating a new Job object. For example, you create a job3
object like so:
let job3 = new Job(
"netflix.com/careers/HOE-role",
true,
"Head of Engineering"
);
console.log(job3)
The code above creates a new job3
object with the wrong order of the properties and is missing the isRemote
property. You would get something like this in the console:
The image above shows what the job3
object will look like when printed out in the console. Notice that the isRemote
is undefined
.
In the next section, let's look at how to add methods to classes.
Class Methods
When creating classes, you can add as many properties as you like. For example, if you have a Vehicle
class, aside from basic properties like type
, color
, brand
, and year
, you probably also want to have methods like start
, stop
, pickUpPassengers
, and dropOffPassengers
.
To add methods inside classes, you can add them after the constructor
function:
class Vehicle {
constructor(type, color, brand, year) {
this.type = type;
this.color = color;
this.brand = brand;
this.year = year;
}
start() {
return "Vroom! Vroom!! Vehicle started";
}
stop() {
return "Vehicle stopped";
}
pickUpPassengers() {
return "Passengers picked up";
}
dropOffPassengers() {
return "Passengers dropped off";
}
}
The code above defines a Vehicle class with type
, color
, brand
, and year
properties, as well as start
, stop
, pickUpPassengers
, and dropOffPassengers
methods.
To run methods on the object, you can use the dot notation:
const vehicle1 = new Vehicle("car", "red", "Ford", 2015);
const vehicle2 = new Vehicle("motorbike", "blue", "Honda", 2018);
console.log(vehicle1.start()); // returns 'Vroom! Vroom!! Vehicle started'
console.log(vehicle2.pickUpPassengers()); // returns "Passengers picked up"
The code above creates two types of vehicles using the Vehicle
class and runs the class methods on them.
Let's look at computed properties in the next section.
Computed Properties
Programming largely depends on changing values, so similar to how you don't want to hardcode most of the values of the class properties, you might have some dynamic property names that vary based on some values. You can use computed properties to do that; let's see how.
For example, when creating a job listing API, you might want developers to be able to change the applyThrough
function name to another word, such as applyThroughLinkedIn
or applyThroughIndeed
, depending on their platform. To use computed properties, you need to wrap the property name in square brackets:
let applyThrough = "applyThroughIndeed";
class Job {
constructor(role, salary, applicationLink, isRemote) {
this.role = role;
this.salary = salary;
this.applicationLink = applicationLink;
this.isRemote = isRemote;
}
[applyThrough]() {
if (applyThrough === "applyThroughLinkedin") {
return `Apply through Linkedin`;
} else if (applyThrough === "applyThroughIndeed") {
return `Apply through Indeed`;
}
}
}
The code above declares the applyThrough
variable with the string value of "applyThroughIndeed"
and a computed method [applyThrough]
that can be called job1.applyThroughIndeed()
.
Computed properties make it easier for other developers to customize their code.
Getters and Setters
When writing code in a team, you want to limit who can change certain parts of the codebase to avoid bugs. It is advisable that in OOP, certain variables and properties should be hidden when necessary.
Next, let's learn how the get
and set
keywords work.
Getters
When building apps that are keen on ensuring users' privacy, for example, legal issues management apps, you want to control who can access users' data like names, emails, and addresses. The get
keyword helps you achieve this. You can limit who can access information:
class Client{
constructor(name, age) {
this._name = name;
this._age = age;
}
get name() {
if (userType === "Lawyer") {
return this._name;
} else {
return "You are not authorized to view this information";
}
}
}
The code above declares a Client class with properties name
and age
and a getter that only returns the name if the user is a Lawyer. If you try to access the name as an Assistant, you'll get an error:
let userType = "Assistant";
const person = new Client("Benjamin Adeleye", 24);
console.log(person.name); // returns 'You are not authorized to view this information'
Note: The
this.name
is changed tothis._name
to avoid naming collisions.
Let's learn about setters next.
Setters
The set
keyword is the opposite of the get
keyword. The get
keyword is used to control who can access properties, while the set
keyword controls who can change the value of properties.
To learn how the set
keyword works, let's extend the previous example by adding a setter:
set name(newName) {
if (userType === "Lawyer" && verifiedData === true) {
this._name = newName;
} else {
console.log("You are not authorized to change this information");
}
}
The above code declares a set
method that allows changes to the name only if the user is a Lawyer and documents have been verified:
let userType = "Lawyer";
let verifiedData = false;
let client = new Client("Benjamin Adeleye", 30);
client.name = "Adeleye Benjamin";
console.log(client.name); // returns 'You are not authorized to change this information'
Note: Methods prefixed with the
get
andset
methods are called getters and setter functions, respectively.
Next, let's take a look at static properties and methods.
Static Values
Sometimes you want to create properties and methods bound to the classes and not the class' instances. For example, you might want a property that counts the number of clients in the database, but you don't want that value bound to the class instances.
Let's look at static properties next.
Static Properties
To track the number of clients in the database, you can use the static
keyword:
static clientCount = 0;
The code above declares a static clientCount
property with the value of 0
. You can access static properties like this:
let cl = new Client("Benjamin Adeleye", 30);
console.log(Client.clientCount); // returns 0
Note: Trying to access static properties using
console.log(cl.clientCount);
would return undefined because static properties are bound to the class and not instances.
Next, let's look at static methods.
Static Methods
Creating static methods is very similar to creating static properties because you only need to prefix the method name with the static
keyword:
static increaseClientCount() {
this.clientCount++;
}
The code above declares a static increaseClientCount
method that increases the clientCount
every time it is called.
Static methods and properties make it easy to create utility and helper functions that can be used directly on the class and not the instances.
Private values
The ECMAScript2022 update shipped with support for private values inside JavaScript classes.
Private fields and methods take encapsulation in classes to the next level because you can now create properties and methods that can only be used inside the class declaration curly braces, and any code outside those curly braces won't be able to access them.
Let's look at private properties next.
Private Properties
You can declare private properties inside a class by prefixing the variable with the #
sign. Let's improve the Client
class in the sections by adding a unique id for every client:
class Client {
#client_unique_id = "";
constructor(name, age, id) {
this._name = name;
this._age = age;
this.#client_unique_id = id;
// same as Client class...
}
The code above declared a private #client_unique_id
variable that can only be used and accessed inside the class declaration. Trying to access it outside the class will return an error:
let cl = new Client("Benjamin Adeleye", 30, "##34505833404494");
console.log(cl.name);
console.log(cl.#client_unique_id); // returns Uncaught SyntaxError: Private field '#client_unique_id' must be declared in an enclosing class
Let's look at private methods next.
Private Methods
As mentioned earlier, private methods are only accessible inside the class declaration. To learn how private methods work, we will add a private method that fetches the client case file documents from the database:
#fetchClientDocs(id) {
return "Fetching client with id: " + id;
}
The code above can now be used inside a public function that the user will call to get the client documents. The essence of the private function is to hide all the underlying authentication and calls to the database from the user or developer that uses the code.
Note: You can also create private static, getter, and setter functions.
Next, let's learn how to chain class methods in the next section.
Method Chaining
As a developer, one of the things you probably enjoy doing most is implementing a feature using as little code as possible. You can do this in JavaScript by chaining methods. For example, when a user logs into your application, you want to change the status of the user to 'online', and when they log out, you change it back to 'offline':
class Twita {
constructor(username, offlineStatus) {
this.username = username;
this.offlineStatus = offlineStatus;
}
login() {
console.log(`${this.username} is logged in`);
return this;
}
setOnlineStatus() {
// set the online status to true
this.offlineStatus = false;
console.log(`${this.username} is online`);
return this;
}
setOfflineStatus() {
// set the offline status to true
this.offlineStatus = true;
console.log(`${this.username} is offline`);
return this;
}
logout() {
console.log(`${this.username} is logged out`);
return this;
}
}
The code above declares a Twita
class with username
and offlineStatus
properties and login
, logout
, setOnlineStatus
, and setOfflineStatus
methods. To chain the methods, you can use the dot notation:
const user = new Twita("Adeleye", true);
twita.login().setOnlineStatus().logout().setOfflineStatus();
The code above will run all the functions sequentially on the user
object and return a response:
The image shows the result of the method chain.
Note: You need to return the current object by returning
this
at the end of each function.
Next, let's look at how to build on existing classes through inheritance.
Class Inheritance
When working with objects, you'll most likely run into situations where you need to create an object that is really similar to an object that already exists in your code, but you know they can't be the same. For example, when building an e-commerce application, you will have a Product
class with properties like name
, price
, description
, and image
and methods like formatPrice
and addToCart
.
However, what if you have multiple types of products with different specifications:
- Books with author, weight, and page details.
- Furniture with length, width, height, and wood-type details.
- Movies CDs with size, duration, and content details.
In this case, creating a class for each product will result in a lot of code duplication, which is one of the major rules of OOP and programming in general - don't repeat yourself (DRY).
Class inheritance allows you to create objects based on other objects. For example, you can solve the problem mentioned above by creating a Product
class:
class Product {
constructor(name, price, description, image) {
this.name = name;
this.price = price;
this.description = description;
this.image = image;
}
formatprice() {
return `${this.price}$`;
}
addToCart() {
cart.add(this);
}
}
Then, create a subclass for each product type using the extends
keyword:
class Book extends Product {
constructor(name, price, description, image, weight, pages, author) {
super(name, price, description, image);
this.author = author;
this.weight = weight;
this.pages = pages;
}
}
class Movie extends Product {
constructor(
name,
price,
description,
image,
size,
contentDescription,
duration
) {
super(name, price, description, image);
this.size = size;
this.contentDescription = contentDescription;
this.duration = duration;
}
}
class Furniture extends Product {
constructor(
name,
price,
description,
image,
woodType,
length,
height,
width
) {
super(name, price, description, image);
this.woodType = woodType;
this.length = length;
this.height = height;
this.width = width;
}
}
The code above declares the Book
, Movie
, and Furniture
product types by extending the Product
class.
There are two new keywords in the above code: extends
and super
. Let's look at them in the next sections.
The extends
Keyword
The extends
keyword is self-explanatory; it is used to extend the capability of another class. In our case, we used it to create the Book
, Movie
, and Furniture
classes by extending the Product
class.
The super
Keyword
The super
keyword eliminates the multiple declarations you would otherwise need to repeat for each new child class. For example, the super
function called in the above code replaced the following code:
this.name = name;
this.price = price;
this.description = description;
this.image = image;
Remember DRY? the reason for this is not to repeat the above code because it has been written inside the Product
class.
The super
function can be omitted if the child class does not need a constructor:
class Animal {
constructor(name, species, color) {
this.name = name;
this.species = species;
this.color = color;
}
makeSound() {
console.log(`${this.name} makes a noise.`);
}
}
class Bird extends Animal {
fly() {
console.log(`${this.name} flies.`);
}
}
The code above declares a parent Animal
class and a child Bird
class that does not need a constructor to work because it is not declaring any new variable inside the constructor. Therefore, the following code should work:
const bird = new Bird('Chloe', 'Parrot', 'Green');
console.log(`${bird.name} is a ${bird.color} ${bird.species}`);
The code above will work even though there's nothing like name
, color
, or species
inside the Bird
class:
You don't need to call super
or repeat the same constructor if you only add methods to the child class.
Conclusion
In this tutorial, you learned about object-oriented programming and classes in JavaScript and how it helps to keep your code clean, DRY, and reusable. We covered the four core concepts of object-oriented programming, including abstraction, encapsulation, inheritance, and polymorphism.
You also learned that using object-oriented programming paradigms and classes in your code has a lot of advantages, from improving application structure and code reusability to reducing code complexity.
Whoa! That was a long ride. Thanks for reading.
Top comments (0)