A Beginner's Guide to Object-Oriented Programming in JavaScript
Learn Object-Oriented Programming Basics in JavaScript
JavaScript was often considered a language primarily suited for scripting interactions on web pages—dominating the front-end development world with its event-driven and functional programming style. However, as JavaScript evolved, so did its programming paradigms. With the introduction of ES6 and beyond, JavaScript now offers cleaner syntax and structures that make it more straightforward to write Object-Oriented Programming (OOP) code. Today, JavaScript comfortably supports classes, inheritance, encapsulation, and more.
In this post, we’ll dive deep into the world of OOP in JavaScript. We’ll start with the basics of objects, walk through prototype-based inheritance, and then explore modern class syntax, constructors, and various OOP principles such as encapsulation, inheritance, and polymorphism.
What is object-oriented programming?
Object-Oriented Programming (OOP) is a programming paradigm centered around the concept of “objects” rather than functions and logic alone. Objects represent entities that combine both data (properties) and behavior (methods) into a single, self-contained unit. OOP aims to make code more modular, extensible, and easier to maintain by modeling real-world concepts directly within the codebase.
Core concepts of OOP include:
Encapsulation: grouping data and functions together into logical units—objects—that keep internal data safe and accessible only through well-defined interfaces.
Abstraction: hiding complex internal details and showing only the necessary features of an object.
Inheritance: Deriving new objects (classes) from existing ones, allowing for code reuse and hierarchical classifications.
Polymorphism: Providing a single interface for different underlying data types, allowing objects to be treated uniformly while still preserving their specialized behavior.
OOP in JavaScript: A Brief Historical Context
Before ES6 (ECMAScript 2015), JavaScript didn’t have a built-in class keyword. Instead, JavaScript relied on a prototype-based inheritance model. Every object in JavaScript is linked to a prototype object from which it can inherit properties. While incredibly powerful and flexible, this prototype-based model often confused developers coming from class-based languages like Java, C#, or C++.
The introduction of the class
syntax in ES6 brought a more familiar and “classical” feel to OOP in JavaScript, even though under the hood it still uses prototypes. The new syntax made JavaScript more approachable for developers used to classical inheritance while still retaining JavaScript’s dynamic and flexible nature.
Objects: The Building Blocks
In JavaScript, almost everything is an object, or can behave like one. An object is simply a collection of key-value pairs. Here’s a simple example:
const person = {
name: 'Alice',
age: 25,
greet: function() {
console.log(`Hello, my name is ${this.name}!`);
}
};
person.greet(); // "Hello, my name is Alice!"
This person
object encapsulates data (name, age) and a behavior (greet). Notice how this
refers to the current object instance, making it straightforward to access its own properties.
Prototype-Based Inheritance: The Traditional Approach
Before class
syntax, inheritance in JavaScript was traditionally managed through prototypes. When you access a property on an object, JavaScript will look it up in the object itself, and if it doesn’t find it, it will look for it in the object’s prototype.
For example:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}!`);
};
const alice = new Person('Alice', 25);
alice.greet(); // "Hello, my name is Alice!"
Here, Person
acts like a constructor function. Instances created by new Person()
inherit thegreet
method from Person.prototype
. If we create const bob = new Person('Bob', 30);
, bob also has greet()
even though greet
isn’t defined directly on bob
.
To extend this idea, we can create another constructor that inherits from Person
:
function Employee(name, age, position) {
Person.call(this, name, age);
this.position = position;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.work = function() {
console.log(`${this.name} is working as a ${this.position}.`);
};
const charlie = new Employee('Charlie', 28, 'Engineer');
charlie.greet(); // Inherited from Person
charlie.work(); // Defined in Employee
Although effective, this approach is verbose and less intuitive compared to class-based languages.
Modern Class Syntax
With ES6, JavaScript introduced the class
keyword, which provides a cleaner and more familiar syntax for creating objects and handling inheritance. Under the hood, it’s still using prototypes, but now we have a more elegant approach.
Defining Classes
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}!`);
}
}
const alice = new Person('Alice', 25);
alice.greet(); // "Hello, my name is Alice!"
In this example:
class Person
defines a new class.The
constructor
method is a special method that runs automatically when a new instance is created.Methods defined inside the class body (
greet()
) are automatically placed on thePerson.prototype
, ensuring all instances share that method.
Inheritance with Classes
We can use the extends
keyword to create subclasses:
class Employee extends Person {
constructor(name, age, position) {
super(name, age); // calls the Person constructor
this.position = position;
}
work() {
console.log(`${this.name} is working as a ${this.position}.`);
}
}
const charlie = new Employee('Charlie', 28, 'Engineer');
charlie.greet(); // Inherited from Person: "Hello, my name is Charlie!"
charlie.work(); // "Charlie is working as a Engineer."
class Employee extends Person
tells JavaScript thatEmployee
is a subclass ofPerson
.We call
super(name, age)
inside theEmployee
constructor to initialize thePerson
part of our object.super
refers to the parent class.Employee
now inherits all properties and methods ofPerson
, plus it adds its own.
OOP Principles in Action
Encapsulation
Encapsulation means bundling data and methods that operate on that data within one unit. In traditional OOP languages, encapsulation often involves controlling access to properties using public, protected, and private keywords. JavaScript classes don’t have built-in access modifiers in the same way, but with modern JavaScript (ES2022 and beyond), we have private class fields:
class BankAccount {
#balance; // private field
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
withdraw(amount) {
if (amount <= this.#balance) {
this.#balance -= amount;
} else {
console.log('Insufficient funds.');
}
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Private fields (#balance
) cannot be accessed or modified directly outside the class. This feature is relatively new and provides true encapsulation.
Inheritance
We demonstrated inheritance above. It lets one class build upon another, promoting code reuse and logical structure. Inheritance also encourages hierarchical thinking — Employee
is a Person
with additional traits.
Polymorphism
Polymorphism means the same method name can behave differently depending on the object calling it. For instance, if you have a describe()
method in a parent class and you override it in a subclass, each object’s describe()
method behaves according to its own class definition:
class Shape {
describe() {
console.log('This is a generic shape.');
}
}
class Circle extends Shape {
describe() {
console.log('This is a circle.');
}
}
class Square extends Shape {
describe() {
console.log('This is a square.');
}
}
const shapes = [new Shape(), new Circle(), new Square()];
shapes.forEach(shape => shape.describe());
// "This is a generic shape."
// "This is a circle."
// "This is a square."
Despite calling describe()
on each shape, the behavior differs depending on the instance’s class, showing polymorphic behavior.
When to Use OOP in JavaScript
OOP is not always the best approach for every problem. JavaScript is a multi-paradigm language, meaning you can also use functional programming (FP), procedural, or a mix. OOP shines when:
You have complex data structures and behaviors that naturally fit into entities (objects).
You want to model real-world objects or hierarchical relationships.
Your codebase can benefit from encapsulation, modularity, and reusability.
For smaller scripts or highly functional tasks, FP might be simpler and more direct. Choose the paradigm that makes your code more understandable and maintainable.
Conclusion
JavaScript’s flexibility enables developers to adopt various programming styles. With modern syntax and language features, writing object-oriented JavaScript code has never been more intuitive. Classes, constructors, inheritance, private fields, and polymorphism allow you to structure code in a way that’s familiar to those coming from classical OOP languages while still taking advantage of JavaScript’s unique strengths.
Whether you’re building small web components or architecting large-scale applications, understanding OOP principles in JavaScript gives you another powerful tool to shape your code into maintainable, modular, and robust software.
Further Reading & Resources:
Embrace the OOP paradigm when it fits your project’s needs, and enjoy the clarity and structure it can bring to your JavaScript code.
📌 Stay Updated
Follow me for more design tips and tools! ✨
🐙 GitHub: Follow me for more web development resources.
🔗 LinkedIn: Connect with me for tips and tricks in coding and productivity.
✍️ Medium: Follow me for in-depth articles on web development and productivity.