Skip to main content

Command Palette

Search for a command to run...

OOP in JavaScript: A Beginner's Guide

Master classes, objects, constructors, and encapsulation in JavaScript — with hands-on exercises and real-world examples.

Updated
OOP in JavaScript: A Beginner's Guide
M

👋 Hey, I'm Mohd Kaif – a student documenting my journey through code. I write about what I'm learning in real-time – the wins, the struggles, and the "aha!" moments. From JavaScript and React to backend systems with Node.js, databases, DevOps, TypeScript, and AI integrations. This blog is my public learning journal: honest, evolving, and always exploring. If you're curious about any of these topics, let's learn and build together!

Have you ever looked at your own code a few weeks later and thought — "Who wrote this mess?"

You scroll through hundreds of lines of loosely connected variables and functions, trying to trace where something broke. It's frustrating, slow, and feels like untangling headphone cables in the dark.

If any of these sound familiar, you're in the right place:

  • "My code works, but adding one new feature feels like defusing a bomb."

  • "I keep copy-pasting the same logic in different places."

  • "I've heard of OOP, but I don't know when or why to actually use it."

  • "I understand classes in theory, but I can't connect them to real code I'd actually write."

The problem isn't that you're a bad developer. The problem is that no one showed you a clean mental model for organizing code as it grows. Object-Oriented Programming (OOP) is that model — and once it clicks, you'll never look at a codebase the same way again.


✅ What You'll Learn

  • What OOP actually means — beyond the textbook definition

  • How to use the blueprint analogy to make classes and objects feel obvious

  • How to write a class in JavaScript, step by step

  • What constructors and methods do, and how this ties them together

  • Why encapsulation matters — with a before/after code comparison

  • When to use OOP (and when not to)

🚫 No prerequisites required. If you know basic JavaScript — variables, functions, and loops — you're ready.


What Is Object-Oriented Programming?

At its core, OOP is a way of organizing code around objects — bundles that combine related data and behavior in one place.

Instead of having scattered variables (studentName, studentAge) and disconnected functions (printStudentDetails()), you group everything that belongs together into a single, tidy unit.

The payoff? Your code starts to mirror the real world — and the real world is a lot easier to reason about than a pile of loose functions.


The Blueprint Analogy (And Why It Works So Well)

Think about how cars are manufactured.

An engineer doesn't build each car from scratch by hand. They design a blueprint once — specifying the engine size, color options, and features. Then that same blueprint is used to produce hundreds of cars. Each car is its own physical object, with its own color and mileage, but they all share the same underlying structure.

That's exactly how classes and objects work in JavaScript.

Real World JavaScript
Blueprint Class
A specific car Object (Instance)
Features (color, speed) Properties
Actions (drive, brake) Methods
        Car (Blueprint / Class)
        ───────────────────────
        brand
        speed
        drive()

               ↓  new Car(...)

   ─────────────────────────────────
   |               |               |
Car #1          Car #2          Car #3
brand: Toyota   brand: BMW     brand: Ford
speed: 120      speed: 150     speed: 130

Each car is its own independent object — but they all share the same structure defined by the class.


Writing Your First Class

Here's how that blueprint looks in JavaScript:

class Car {
  constructor(brand, speed) {
    this.brand = brand;
    this.speed = speed;
  }

  drive() {
    console.log(`\({this.brand} is driving at \){this.speed} km/h`);
  }
}

Let's break down what each part does:

  • class Car — declares the blueprint

  • constructor(...) — runs automatically when you create a new car; sets its initial data

  • this.brand — refers to this specific car's brand (not the blueprint's)

  • drive() — a method that defines what any car can do

🔑 Key insight: this always refers to the specific object being worked with — like saying "this particular car" instead of "cars in general."


Creating Objects From a Class

Once you have a class, you create objects using the new keyword:

const car1 = new Car("Toyota", 120);
const car2 = new Car("BMW", 150);

car1.drive(); // Toyota is driving at 120 km/h
car2.drive(); // BMW is driving at 150 km/h

Each call to new Car(...) uses the blueprint to stamp out a fresh object with its own data. Change car1's speed and car2 is completely unaffected — they're independent objects.

⚠️ Common mistake: Forgetting new is one of the most frequent bugs beginners run into.

const car = Car("Toyota", 120); // ❌ Won't work as expected
const car = new Car("Toyota", 120); // ✅ Correct

🏋️ Quick Exercise #1

Try this before reading on:

  1. Create a class called Animal with properties name and sound

  2. Add a method called speak() that logs: "[name] says [sound]"

  3. Create two animal objects — a dog and a cat — and call speak() on each

✅ See the answer

class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    console.log(`\({this.name} says \){this.sound}`);
  }
}

const dog = new Animal("Dog", "Woof");
const cat = new Animal("Cat", "Meow");

dog.speak(); // Dog says Woof
cat.speak(); // Cat says Meow

The Constructor Method, Up Close

The constructor is a special method — it's the first thing that runs when you create a new object. Think of it as the car's spec sheet being filled in at the factory.

constructor(brand, speed) {
  this.brand = brand; // Assign brand to this specific car
  this.speed = speed; // Assign speed to this specific car
}

A few things worth knowing:

  • If you don't write a constructor, JavaScript creates a silent, empty one for you

  • You can set default values right in the constructor

  • You can run any setup logic here — not just property assignments

class Car {
  constructor(brand, speed = 100) { // Default speed: 100
    this.brand = brand;
    this.speed = speed;
    this.mileage = 0; // Always starts at 0
  }
}

const myCar = new Car("Honda"); // No speed needed
console.log(myCar.speed);   // 100
console.log(myCar.mileage); // 0

Methods: Giving Objects Behavior

Methods are functions that live inside a class. They define what objects of that class can do.

Here's a Student class that we'll build on throughout the rest of this article:

class Student {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  printDetails() {
    console.log(`Name: \({this.name}, Age: \){this.age}`);
  }
}

You can add as many methods as you need:

class Student {
  constructor(name, age, grade) {
    this.name = name;
    this.age = age;
    this.grade = grade;
  }

  printDetails() {
    console.log(`Name: \({this.name}, Age: \){this.age}`);
  }

  isPassingGrade() {
    return this.grade >= 50;
  }

  describe() {
    const status = this.isPassingGrade() ? "passing" : "failing";
    console.log(`\({this.name} is currently \){status}.`);
  }
}

const student1 = new Student("Rahul", 20, 72);
const student2 = new Student("Priya", 22, 45);

student1.describe(); // Rahul is currently passing.
student2.describe(); // Priya is currently failing.

Notice how methods can call other methods inside the same class using this. The describe() method calls this.isPassingGrade() — keeping logic clean and reusable.


🏋️ Quick Exercise #2

Extend the Student class above:

  1. Add a property called courses (an array of course names) in the constructor

  2. Add a method enroll(course) that adds a course to the array

  3. Add a method listCourses() that logs all enrolled courses

  4. Create a student, enroll them in two courses, and call listCourses()

✅ See the answer

class Student {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.courses = [];
  }

  enroll(course) {
    this.courses.push(course);
    console.log(`\({this.name} enrolled in \){course}`);
  }

  listCourses() {
    console.log(`\({this.name}'s courses: \){this.courses.join(", ")}`);
  }
}

const s = new Student("Aman", 19);
s.enroll("JavaScript");
s.enroll("HTML & CSS");
s.listCourses(); // Aman's courses: JavaScript, HTML & CSS

Encapsulation: The Part Most Tutorials Skip

Encapsulation is the idea of bundling data and methods together, and controlling how that data is accessed or changed.

This might sound abstract, so let's make it concrete with a before/after example.

❌ Without Encapsulation

let balance = 1000;

function deposit(amount) {
  balance += amount;
}

function withdraw(amount) {
  balance -= amount;
}

// Nothing stops someone from doing this:
balance = -99999; // 💣 Direct manipulation — no safeguards

With scattered variables and functions, anything can touch your data. One accidental assignment and your balance is -$99,999.

✅ With Encapsulation

class BankAccount {
  constructor(initialBalance) {
    this.balance = initialBalance;
  }

  deposit(amount) {
    if (amount <= 0) {
      console.log("Deposit amount must be positive.");
      return;
    }
    this.balance += amount;
    console.log(`Deposited $${amount}. New balance: $${this.balance}`);
  }

  withdraw(amount) {
    if (amount > this.balance) {
      console.log("Insufficient funds.");
      return;
    }
    this.balance -= amount;
    console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
  }

  getBalance() {
    return this.balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);   // Deposited \(500. New balance: \)1500
account.withdraw(200);  // Withdrew \(200. New balance: \)1300
account.withdraw(9999); // Insufficient funds.

Now the balance is only changed through controlled methods that include validation logic. No outside code can randomly set balance = -99999 and break everything.

Why encapsulation matters:

  • Prevents accidental (or malicious) data corruption

  • Makes your code's behavior predictable

  • Easier to debug — you know exactly where data can change


The Full Student Example, Start to Finish

Let's put everything together. Here's the complete Student class, building every concept we've covered:

class Student {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.courses = [];
  }

  printDetails() {
    console.log(`Name: \({this.name}, Age: \){this.age}`);
  }

  enroll(course) {
    this.courses.push(course);
  }

  listCourses() {
    if (this.courses.length === 0) {
      console.log(`${this.name} has no enrolled courses.`);
      return;
    }
    console.log(`\({this.name}'s courses: \){this.courses.join(", ")}`);
  }
}

// Create multiple student objects
const student1 = new Student("Rahul", 20);
const student2 = new Student("Priya", 22);
const student3 = new Student("Aman", 19);

student1.enroll("JavaScript");
student1.enroll("Node.js");

student2.enroll("Python");

student1.printDetails(); // Name: Rahul, Age: 20
student1.listCourses();  // Rahul's courses: JavaScript, Node.js

student2.printDetails(); // Name: Priya, Age: 22
student2.listCourses();  // Priya's courses: Python

student3.printDetails(); // Name: Aman, Age: 19
student3.listCourses();  // Aman has no enrolled courses.
        Student (Class)
        ───────────────────────────────
        name
        age
        courses []
        printDetails()
        enroll()
        listCourses()

                     ↓  new Student(...)

   ─────────────────────────────────────────────
   |                  |                        |
Student1           Student2               Student3
name: "Rahul"      name: "Priya"          name: "Aman"
age: 20            age: 22                age: 19
courses: [JS, Node] courses: [Python]     courses: []

🏋️ Quick Exercise #3 — Your Turn

Build a Library class from scratch:

  1. It should have a property books (an array), initialized as empty

  2. Add an addBook(title) method that adds a book to the array

  3. Add a listBooks() method that logs all available books

  4. Add a removeBook(title) method that removes a book by title

  5. Create a library, add 3 books, remove 1, and list what's left

This is the kind of real-world object you'd model in an actual app — give it a try!

✅ See the answer

class Library {
  constructor() {
    this.books = [];
  }

  addBook(title) {
    this.books.push(title);
    console.log(`"${title}" added to the library.`);
  }

  removeBook(title) {
    const index = this.books.indexOf(title);
    if (index === -1) {
      console.log(`"${title}" not found.`);
      return;
    }
    this.books.splice(index, 1);
    console.log(`"${title}" removed.`);
  }

  listBooks() {
    console.log("Available books:", this.books.join(", "));
  }
}

const lib = new Library();
lib.addBook("JavaScript: The Good Parts");
lib.addBook("Clean Code");
lib.addBook("You Don't Know JS");
lib.removeBook("Clean Code");
lib.listBooks(); // JavaScript: The Good Parts, You Don't Know JS

When Should You Use OOP?

OOP is a powerful tool — but not every nail needs this hammer.

Use OOP when:

  • You're modeling real-world entities (Users, Products, Orders, Accounts)

  • You need multiple instances of something with the same structure

  • Data and behavior naturally belong together

  • Your codebase is growing and needs organized structure

Skip OOP when:

  • You're writing a small, one-off script

  • The logic is purely functional (transforming data, filtering arrays)

  • Adding a class would make things more complex, not less

The goal of OOP isn't to use classes everywhere — it's to use them where they genuinely make your code cleaner and more maintainable.


What to Learn Next

Now that you understand classes, objects, constructors, methods, and encapsulation, here's where to go next:

  1. Inheritance — Learn how one class can extend another to reuse and build on existing logic (e.g., a GraduateStudent that inherits from Student)

  2. Prototypes — Understand what's happening under the hood when JavaScript creates objects, so class behavior never surprises you

  3. Polymorphism — Discover how different objects can share the same method name but behave differently — a powerful pattern in real systems

  4. Private Fields (#) — Take encapsulation further with JavaScript's native syntax for truly private properties that can't be touched from outside a class

Each of these builds directly on what you've just learned — you're already halfway there.


💬 Got Questions?

Drop a comment below! I'd love to hear what you're building or help you work through anything that didn't click.

Here are topics coming up in future articles:

  • Inheritance in JavaScript: How to build child classes that reuse and extend parent logic without repeating yourself

  • Understanding Prototypes: What's really happening when you write new, and how JavaScript's prototype chain actually works

  • OOP Design Patterns: Practical patterns like Factory, Singleton, and Observer that show up in real codebases

  • Building a Mini-App with OOP: A hands-on project that ties classes, encapsulation, and inheritance together into something you can ship


Found this helpful? Share it with someone just getting started with JavaScript — and follow along so you don't miss the next one. 🚀

Zero to Full Stack Developer: From Basics to Production

Part 30 of 50

Complete full-stack web development series from zero to production. Learn HTML, CSS, JavaScript, TypeScript, React, Next.js, Node.js, databases, Docker, AWS, and AI integration. Build real-world projects step-by-step.

Up next

JavaScript's this Keyword Explained

How to control function context with call(), apply(), and bind() — with hands-on examples and common mistakes