πŸ“– Introduction to JavaScript OOP

Up until now, you've focused on Procedural Programming, which involves writing sequences of instructions for the computer to follow, and Functional Programming, which emphasizes writing pure functions and avoiding changing state. Object-Oriented Programming (OOP) introduces a new way of thinking about how to structure and organize your code.

JavaScript Object Literals

In JavaScript, there are different ways to create and manage objects. The most common methods include using object literals and using ES6 classes. While both approaches allow you to define objects with properties and methods, they differ in their structure, flexibility, and how they handle inheritance.

Object Literals

Object literals are a straightforward way to create single objects in JavaScript. They use a simple syntax where you define the object's properties and methods within a pair of curly braces. This approach is quick and easy for creating individual objects but can become cumbersome when you need to create multiple similar objects or implement inheritance.


// Creating an object using an object literal
const book = {
    title: "1984",
    author: "George Orwell",
    isBorrowed: false,
    
    borrow() {
        this.isBorrowed = true;
        console.log(`${this.title} has been borrowed.`);
    },
    
    returnBook() {
        this.isBorrowed = false;
        console.log(`${this.title} has been returned.`);
    }
};

// Accessing properties and methods
console.log(book.title); // Outputs: 1984
book.borrow();           // Outputs: 1984 has been borrowed.

Object literals are useful for creating simple objects quickly. However, if you need to create multiple objects with the same structure or implement inheritance, this method has its limitations. Each object created with an object literal is independent, and there’s no built-in way to share behavior (methods) between them unless you use prototypes.

ES6 Classes

ES6 classes provide a more structured way to create objects in JavaScript. A class serves as a blueprint for creating multiple objects with the same properties and methods. Classes also make it easier to implement inheritance, allowing you to create hierarchies of related objects that share behavior.

With ES6 classes, you can create multiple objects (instances) from the same class. These instances share the same structure and behavior, defined once in the class. Classes also simplify the process of creating and managing inheritance, making it easier to build complex applications with related objects.

OOP Structure

In OOP, the focus shifts from writing functions and procedures to creating objects that represent real-world entities or concepts within your application. Instead of writing a series of functions that operate on data, you encapsulate that data within objects, along with the functions (now called methods) that operate on it. This encapsulation allows you to bundle data and behavior together in a way that more closely models how we think about real-world entities.

To give you an idea of what this means, here's a quick overview.

Class
A class is like a blueprint for creating objects. It defines the properties and methods that the objects created from it will have. For example, a Book class might define properties like title and author, and methods like borrow() and returnBook(). When you create an object from a class, you are creating an instance of that class.
Properties
Properties are data that belong to an object. For example, a book object might have properties like title: "1984" and author: "George Orwell", which store the state of the object.
Methods
Methods are functions that belong to an object. They define the behavior of that object and can operate on the object's properties. For instance, a book object might have a method like borrow() that changes the book's state from available to borrowed.

So, the big idea behind OOP is to bring data (properties) and the functions that operate on that data (methods) together into a single unit called an object. These objects are created based on structures defined in classes. By organizing your code this way, you can more easily model complex systems, reduce duplication, and create code that's easier to maintain and extend over time.

A Simple Example: The Book Class

Let's start with a very simple example of a class. The following Book class includes properties for the title and author of the book, as well as methods for borrowing and returning the book.

class Book {
    title = "Unknown Title";
    author = "Unknown Author";
    isBorrowed = false;

    borrow() {
        this.isBorrowed = true;
        console.log(`${this.title} has been borrowed.`);
    }

    returnBook() {
        this.isBorrowed = false;
        console.log(`${this.title} has been returned.`);
    }
}

// Creating an instance of the Book class
const myBook = new Book();

// Accessing properties and using methods
console.log(myBook.title); // Outputs: Unknown Title
myBook.borrow();           // Outputs: Unknown Title has been borrowed.

This simple example shows how a class can define both the data (properties) and the behavior (methods) of an object. The class acts as a blueprint, and each instance of the class (like myBook) has its own set of properties and can use the methods defined in the class.

Introduction to Constructors

As you begin to work with classes in Object-Oriented Programming, you'll often need a way to initialize your objects with specific values when they are created. This is where the constructor comes in.

A constructor is a special method in a class that is automatically called when a new object is created. The primary role of the constructor is to set up the initial state of the object by assigning values to its properties. This allows each instance of a class to start with its own unique set of data.

For example, imagine you want to create multiple books in your application, each with a different title and author. Using a constructor, you can pass in the title and author as arguments when creating each book, ensuring that each book object is initialized with the correct information.

Simple Constructor Example

Here's a simple example of a class with a constructor:

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

    getDetails() {
        return `${this.title} by ${this.author}`;
    }
}

// Creating an instance of the Book class
const myBook = new Book("1984", "George Orwell");
console.log(myBook.getDetails()); // Outputs: 1984 by George Orwell

In this example, the Book class has a constructor that takes two parameters: title and author. When a new book object is created, the constructor assigns these values to the corresponding properties of the object. This ensures that every book object is initialized with its own specific title and author.

Constructors are a fundamental part of working with classes in OOP because they provide a flexible and consistent way to initialize your objects. As you continue to explore OOP, you'll see how constructors can be used to set up objects in more complex and dynamic ways.

Key Concepts in OOP

Object-Oriented Programming revolves around four main concepts, often referred to as the "Four Pillars of OOP".

Encapsulation

Encapsulation is the practice of bundling the data (properties) and the methods (functions) that manipulate the data into a single unit, called a class. This helps to keep related functionality together and protects the data from outside interference and misuse.

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

    getDetails() {
        return `${this.title} by ${this.author}`;
    }
}

In the example above, the Book class encapsulates the title and author properties along with the getDetails method. This approach ensures that the data and functionality related to a book are grouped together, keeping the internal state of the object hidden and safe.

Abstraction

Abstraction is the concept of hiding the complex implementation details of an object and exposing only the essential features to the user. This allows users to interact with objects in a simpler, more meaningful way without needing to understand the underlying complexity.

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

    addBook(book) {
        this.books.push(book);
    }

    listBooks() {
        return this.books.map(book => book.getDetails());
    }
}

The Library class abstracts the complexity of managing a collection of books. Users can add books to the library and list all books without worrying about how the books are stored or managed internally.

Inheritance

Inheritance allows a class to inherit properties and methods from another class, creating a hierarchical relationship. This is done in JavaScript using the extends keyword. When a class extends another class, it gains access to the methods and properties of that class, promoting code reuse and reducing duplication.

class Media {
    constructor(title) {
        this.title = title;
    }

    play() {
        console.log(`Playing ${this.title}`);
    }
}

class Movie extends Media { // 'Movie' class extends 'Media' class
    getDirector() {
        return "Director name not set yet";
    }
}

const myMovie = new Movie("Inception");
myMovie.play(); // Outputs: Playing Inception
console.log(myMovie.getDirector()); // Outputs: Director name not set yet

In this example, the Movie class extends the Media class, meaning it inherits the play method from Media. The Movie class also defines its own method, getDirector, while still having access to the inherited play method.

Polymorphism

Polymorphism enables a single method to operate on different objects in ways that are appropriate to the class of each object, demonstrating the true power of Object-Oriented Programming.

class Shape {
    area() {
        return 0;
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    area() {
        return this.width * this.height;
    }
}

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

    area() {
        return Math.PI * this.radius * this radius;
    }
}

The Shape class has a generic area method, but the Rectangle and Circle classes override this method to calculate the area specific to their shape. Polymorphism allows these different shapes to be used interchangeably, with each providing its own implementation of the area method.

Putting It Into Action

Now that you've learned about the fundamental concepts of Object-Oriented Programming, let's put those concepts into practice with a simple interactive application. In this exercise, you'll create a basic Library Management System where users can add books to a library, list all books, and borrow or return a book.

Follow these steps to create your application:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Library Management System</title>
</head>
<body>

    <h1>Library Management System</h1>

    <h2>Add a New Book</h2>
    <form id="addBookForm">
        <label for="title">Title:</label>
        <input type="text" id="title" name="title"><br><br>

        <label for="author">Author:</label>
        <input type="text" id="author" name="author"><br><br>

        <button type="submit">Add Book</button>
    </form>

    <h2>Books in Library</h2>
    <ul id="bookList"></ul>

    <script>
        class Book {
            constructor(title, author) {
                this.title = title;
                this.author = author;
                this.isBorrowed = false;
            }

            borrow() {
                if (!this.isBorrowed) {
                    this.isBorrowed = true;
                    return `${this.title} has been borrowed.`;
                } else {
                    return `${this.title} is already borrowed.`;
                }
            }

            returnBook() {
                if (this.isBorrowed) {
                    this.isBorrowed = false;
                    return `${this.title} has been returned.`;
                } else {
                    return `${this.title} was not borrowed.`;
                }
            }

            getDetails() {
                return `${this.title} by ${this.author}`;
            }
        }

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

            addBook(book) {
                this.books.push(book);
            }

            listBooks() {
                return this.books.map(book => book.getDetails());
            }
        }

        const library = new Library();

        document.getElementById('addBookForm').addEventListener('submit', function(event) {
            event.preventDefault();
            const title = document.getElementById('title').value;
            const author = document.getElementById('author').value;

            const newBook = new Book(title, author);
            library.addBook(newBook);

            updateBookList();
        });

        function updateBookList() {
            const bookList = document.getElementById('bookList');
            bookList.innerHTML = '';
            library.listBooks().forEach(bookDetails => {
                const li = document.createElement('li');
                li.textContent = bookDetails;
                bookList.appendChild(li);
            });
        }
    </script>
</body>
</html>

This example creates a basic Library Management System where users can add books to a library and view the list of books. The application demonstrates the use of classes, constructors, methods, and how objects can interact with one another in an organized and meaningful way.

Challenge

Now that you've built a basic Library Management System, let's take it a step further.

  • Add functionality to borrow and return books. Create buttons next to each book in the list that allow the user to borrow or return the book.
  • When a book is borrowed, update the list to reflect its status as borrowed.
  • If the user tries to borrow a book that's already borrowed, display an appropriate message.
  • Likewise, if the user tries to return a book that's not borrowed, display a message indicating that the book was not borrowed.

To complete the challenge, you can modify the Book class to include methods for borrowing and returning books, and update the event handling in your JavaScript code to call these methods when the user interacts with the UI.

In order to check your learning, you should attempt to create a solution before revealing the provided solution below.


// JavaScript Code

class Book {
    constructor(title, author) {
        this.title = title;
        this.author = author;
        this.isBorrowed = false;
    }

    borrow() {
        if (!this.isBorrowed) {
            this.isBorrowed = true;
            return `${this.title} has been borrowed.`;
        } else {
            return `${this.title} is already borrowed.`;
        }
    }

    returnBook() {
        if (this.isBorrowed) {
            this.isBorrowed = false;
            return `${this.title} has been returned.`;
        } else {
            return `${this.title} was not borrowed.`;
        }
    }

    getDetails() {
        return `${this.title} by ${this.author}` + (this.isBorrowed ? ' (Borrowed)' : '');
    }
}

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

    addBook(book) {
        this.books.push(book);
    }

    listBooks() {
        return this.books;
    }
}

const library = new Library();

document.getElementById('addBookForm').addEventListener('submit', function(event) {
    event.preventDefault();
    const title = document.getElementById('title').value;
    const author = document.getElementById('author').value;

    const newBook = new Book(title, author);
    library.addBook(newBook);

    updateBookList();
});

function updateBookList() {
    const bookList = document.getElementById('bookList');
    bookList.innerHTML = '';

    library.listBooks().forEach((book, index) => {
        const li = document.createElement('li');
        li.textContent = book.getDetails();

        const borrowButton = document.createElement('button');
        borrowButton.textContent = book.isBorrowed ? 'Return' : 'Borrow';
        borrowButton.addEventListener('click', () => {
            if (book.isBorrowed) {
                alert(book.returnBook());
            } else {
                alert(book.borrow());
            }
            updateBookList();
        });

        li.appendChild(borrowButton);
        bookList.appendChild(li);
    });
}
                

References