codelessgenie guide

Understanding Object-Oriented Programming in JavaScript

Object-Oriented Programming (OOP) is a programming paradigm centered around the concept of "objects" — self-contained units that bundle data (properties) and behavior (methods). Unlike procedural programming, which focuses on functions and sequential execution, OOP emphasizes modeling real-world entities (e.g., users, cars, books) as objects, making code more reusable, maintainable, and intuitive. JavaScript, often hailed as a "multi-paradigm" language, supports OOP but with a unique twist: it uses **prototypal inheritance** instead of the class-based inheritance found in languages like Java or C++. This distinction can confuse developers familiar with traditional OOP, but once understood, it unlocks JavaScript’s flexibility. In this blog, we’ll demystify OOP in JavaScript. We’ll start with core OOP principles, explore how JavaScript implements them, and walk through practical examples to solidify your understanding.

Table of Contents

  1. What is Object-Oriented Programming?
  2. Core Principles of OOP
    • Encapsulation
    • Inheritance
    • Polymorphism
    • Abstraction
  3. JavaScript and OOP: A Unique Approach
  4. Objects in JavaScript: The Building Blocks
  5. Constructors and the new Keyword
  6. ES6 Classes: Syntactic Sugar Over Prototypes
  7. Prototypal Inheritance: How Objects Inherit
  8. Encapsulation in JavaScript
  9. Polymorphism in JavaScript
  10. Abstraction in JavaScript
  11. Practical Example: Building a Library Management System
  12. Conclusion
  13. References

1. What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a design philosophy that organizes code into objects, which are collections of:

  • Properties: Data or attributes that describe the object (e.g., a car object might have color, model, and year).
  • Methods: Functions that define the object’s behavior (e.g., car.drive(), car.brake()).

OOP aims to model real-world systems by breaking them into reusable, self-contained objects. For example, in a banking app, you might have Account objects with properties like balance and methods like deposit() or withdraw().

Why OOP?

  • Reusability: Objects can be reused across projects or parts of an application.
  • Maintainability: Changes to an object’s internal logic don’t affect other parts of the code (encapsulation).
  • Scalability: Complex systems are easier to manage when broken into modular objects.

2. Core Principles of OOP

OOP is built on four foundational principles. Let’s define them and preview how JavaScript implements each:

2.1 Encapsulation

Definition: Bundling data (properties) and methods that operate on that data into a single unit (object), while restricting access to some of the object’s components.

Goal: Protect data from unintended modification and expose only necessary functionality.

JavaScript Implementation: Uses closures, private fields (ES6+), or naming conventions (e.g., _property) to hide internal state.

2.2 Inheritance

Definition: Allowing objects to inherit properties and methods from other objects, promoting code reuse.

Goal: Avoid redundant code by defining shared behavior in a “parent” object and having “child” objects inherit from it.

JavaScript Implementation: Prototypal inheritance (objects inherit directly from other objects) instead of class-based inheritance (common in Java/C#).

2.3 Polymorphism

Definition: The ability of different objects to respond to the same method name in different ways.

Goal: Write flexible code that works with multiple object types without needing to know their specific type.

JavaScript Implementation: Method overriding (child objects redefine parent methods) and dynamic typing.

2.4 Abstraction

Definition: Hiding complex implementation details and exposing only essential features.

Goal: Simplify interaction with objects by focusing on “what” they do, not “how” they do it.

JavaScript Implementation: Using classes or factory functions to define abstract interfaces (e.g., a Shape class with an abstract area() method that subclasses must implement).

3. JavaScript and OOP: A Unique Approach

Unlike class-based languages (e.g., Java), JavaScript is prototype-based. This means:

  • There are no “classes” in the traditional sense (though ES6 introduced class syntax as syntactic sugar).
  • Objects inherit directly from other objects (called “prototypes”), not from classes.
  • Every object has a prototype (a hidden link to another object), forming a prototype chain. If a property/method isn’t found on an object, JavaScript searches its prototype, and so on, until the chain ends.

4. Objects in JavaScript: The Building Blocks

In JavaScript, almost everything is an object (strings, arrays, functions, etc.). Even primitives (like 123 or "hello") are wrapped in temporary objects when accessing methods.

4.1 Object Literals

The simplest way to create an object is with object literal syntax ({}):

// An object representing a book
const book = {
  title: "The Great Gatsby",
  author: "F. Scott Fitzgerald",
  pages: 180,
  isAvailable: true,

  // Method: describes behavior
  getSummary() {
    return `${this.title} by ${this.author}, ${this.pages} pages.`;
  },

  // Accessor method (getter)
  get availability() {
    return this.isAvailable ? "Available" : "Borrowed";
  }
};

console.log(book.getSummary()); // "The Great Gatsby by F. Scott Fitzgerald, 180 pages."
console.log(book.availability); // "Available"

Key Notes:

  • this refers to the object itself (e.g., this.title accesses the title property of book).
  • Methods are functions defined as object properties.
  • Getters/setters (e.g., availability) allow controlled access to properties.

5. Constructors and the new Keyword

To create multiple objects with the same structure (e.g., multiple Book instances), use constructor functions. These are functions designed to initialize objects.

Example: Constructor Function

// Constructor function for Book
function Book(title, author, pages) {
  // Properties initialized with parameters
  this.title = title;
  this.author = author;
  this.pages = pages;
  this.isAvailable = true; // Default value

  // Method (problem: recreated for every instance!)
  this.getSummary = function() {
    return `${this.title} by ${this.author}`;
  };
}

// Create instances with the `new` keyword
const book1 = new Book("1984", "George Orwell", 328);
const book2 = new Book("To Kill a Mockingbird", "Harper Lee", 336);

console.log(book1.getSummary()); // "1984 by George Orwell"
console.log(book2.title); // "To Kill a Mockingbird"

How new Works:

  1. Creates a new empty object.
  2. Sets the new object’s prototype to the constructor’s prototype property.
  3. Binds this to the new object and runs the constructor.
  4. Returns the new object (unless the constructor explicitly returns another object).

6. ES6 Classes: Syntactic Sugar Over Prototypes

ES6 (2015) introduced class syntax to simplify object creation and inheritance. Despite the name, JavaScript classes are not traditional classes—they are just syntactic sugar over prototype-based inheritance.

Example: ES6 Class

class Book {
  // Constructor initializes properties
  constructor(title, author, pages) {
    this.title = title;
    this.author = author;
    this.pages = pages;
    this.isAvailable = true;
  }

  // Method (added to the prototype, shared by all instances)
  getSummary() {
    return `${this.title} by ${this.author}`;
  }

  // Static method (belongs to the class, not instances)
  static comparePages(book1, book2) {
    return `${book1.title} has ${book1.pages} pages; ${book2.title} has ${book2.pages} pages.`;
  }
}

// Create instances
const book1 = new Book("1984", "George Orwell", 328);
const book2 = new Book("To Kill a Mockingbird", "Harper Lee", 336);

console.log(book1.getSummary()); // "1984 by George Orwell"
console.log(Book.comparePages(book1, book2)); // "1984 has 328 pages; To Kill a Mockingbird has 336 pages."

Key Notes:

  • constructor: Runs when a new instance is created (initializes properties).
  • Methods like getSummary are added to the class’s prototype, so all instances share them (unlike constructor functions, which recreate methods per instance).
  • static methods (e.g., comparePages) are called on the class itself, not instances.

7. Prototypal Inheritance: How Objects Inherit

In JavaScript, inheritance is implemented via the prototype chain. Every object has a prototype (a reference to another object). When you access a property/method on an object, JavaScript first checks the object itself. If not found, it searches the prototype, then the prototype’s prototype, and so on.

Example: Prototypal Inheritance with extends

ES6 class syntax simplifies inheritance with extends and super:

// Parent class
class Animal {
  constructor(name) {
    this.name = name;
    this.isAlive = true;
  }

  eat() {
    return `${this.name} is eating.`;
  }
}

// Child class inheriting from Animal
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }

  bark() { // Child-specific method
    return `${this.name} barks: Woof!`;
  }

  // Override parent method
  eat() {
    return `${this.name} (a ${this.breed}) is eating kibble.`;
  }
}

const myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.name); // "Buddy" (inherited from Animal)
console.log(myDog.bark()); // "Buddy barks: Woof!" (child method)
console.log(myDog.eat()); // "Buddy (a Golden Retriever) is eating kibble." (overridden)

Prototype Chain in Action:

myDogDog.prototypeAnimal.prototypeObject.prototypenull (end of chain).

8. Encapsulation in JavaScript

Encapsulation ensures internal state is hidden, and only public methods can modify it. JavaScript has evolved to support true encapsulation:

1. Naming Conventions (Weak Encapsulation)

Use a leading underscore (_) to signal “private” properties (convention only—still accessible):

class Counter {
  constructor() {
    this._count = 0; // "Private" by convention
  }

  increment() {
    this._count++;
  }

  getCount() {
    return this._count;
  }
}

const counter = new Counter();
counter._count = 100; // Still possible (no true privacy)

2. ES6 Private Fields (True Encapsulation)

Use # to define private fields (ES2022+), which are inaccessible from outside the class:

class Counter {
  #count = 0; // Private field (truly inaccessible)

  increment() {
    this.#count++;
  }

  getCount() {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // 1
counter.#count = 100; // Error: Private field '#count' must be declared in an enclosing class

9. Polymorphism in JavaScript

Polymorphism allows objects of different types to respond to the same method name. In JavaScript, this is enabled by dynamic typing and method overriding.

Example: Polymorphism with draw()

class Shape {
  draw() {
    return "Drawing a shape.";
  }
}

class Circle extends Shape {
  draw() {
    return "Drawing a circle with a radius.";
  }
}

class Square extends Shape {
  draw() {
    return "Drawing a square with sides.";
  }
}

// Polymorphic function: works with any Shape
function renderShape(shape) {
  console.log(shape.draw());
}

// Pass different Shape types to renderShape
renderShape(new Shape()); // "Drawing a shape."
renderShape(new Circle()); // "Drawing a circle with a radius."
renderShape(new Square()); // "Drawing a square with sides."

Here, renderShape works with Shape, Circle, and Square because they all implement draw().

10. Abstraction in JavaScript

Abstraction hides complexity by exposing only essential features. In JavaScript, you can define abstract “interfaces” using classes with unimplemented methods (subclasses must implement them).

Example: Abstract Shape Class

class Shape {
  // Abstract method (subclasses must implement)
  area() {
    throw new Error("Subclasses must implement the 'area' method.");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() { // Implement abstract method
    return Math.PI * this.radius ** 2;
  }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }

  area() { // Implement abstract method
    return this.side ** 2;
  }
}

const circle = new Circle(5);
console.log(circle.area()); // ~78.54

const square = new Square(4);
console.log(square.area()); // 16

const shape = new Shape();
shape.area(); // Error: Subclasses must implement the 'area' method.

Here, Shape defines the abstract area() method, and Circle/Square implement it with their own logic.

11. Practical Example: Building a Library Management System

Let’s tie together OOP concepts with a simple library system. We’ll create:

  • A Book class with private fields and methods to borrow/return books.
  • A Library class to manage books.
class Book {
  #title; // Private field
  #author;
  #isBorrowed = false; // Default: not borrowed

  constructor(title, author) {
    this.#title = title;
    this.#author = author;
  }

  // Public method to borrow the book
  borrow() {
    if (this.#isBorrowed) {
      return `${this.#title} is already borrowed.`;
    }
    this.#isBorrowed = true;
    return `You borrowed ${this.#title}.`;
  }

  // Public method to return the book
  return() {
    if (!this.#isBorrowed) {
      return `${this.#title} is not borrowed.`;
    }
    this.#isBorrowed = false;
    return `You returned ${this.#title}.`;
  }

  // Public getter for title (read-only)
  get title() {
    return this.#title;
  }

  // Check availability
  isAvailable() {
    return !this.#isBorrowed;
  }
}

class Library {
  #books = []; // Private array to store books

  addBook(book) {
    if (book instanceof Book) {
      this.#books.push(book);
      return `Added "${book.title}" to the library.`;
    }
    return "Invalid book.";
  }

  listAvailableBooks() {
    const available = this.#books.filter(book => book.isAvailable());
    return available.map(book => book.title).join(", ");
  }
}

// Usage
const library = new Library();
const book1 = new Book("1984", "George Orwell");
const book2 = new Book("To Kill a Mockingbird", "Harper Lee");

console.log(library.addBook(book1)); // "Added "1984" to the library."
console.log(library.addBook(book2)); // "Added "To Kill a Mockingbird" to the library."

console.log(book1.borrow()); // "You borrowed 1984."
console.log(library.listAvailableBooks()); // "To Kill a Mockingbird"

console.log(book1.return()); // "You returned 1984."
console.log(library.listAvailableBooks()); // "1984, To Kill a Mockingbird"

Key Concepts Demonstrated:

  • Encapsulation: Book uses private fields (#title, #isBorrowed) and controls access via methods.
  • Abstraction: Book hides how #isBorrowed is modified (only via borrow()/return()).
  • Reusability: Library manages multiple Book instances.

12. Conclusion

Object-Oriented Programming in JavaScript is a powerful paradigm built on prototypes, not classes. While ES6 class syntax simplifies syntax, under the hood, JavaScript relies on prototype chains for inheritance. By mastering core OOP principles—encapsulation, inheritance, polymorphism, and abstraction—you can write modular, reusable, and maintainable code.

Remember: JavaScript’s prototype-based model offers flexibility, but it’s critical to understand how the prototype chain works to avoid pitfalls. Practice with real-world examples (like the library system above) to solidify your skills!

13. References