advanced Step 16 of 20

Classes and OOP

JavaScript Programming

Classes and OOP

ES6 classes provide a clean, familiar syntax for object-oriented programming in JavaScript. Under the hood, classes are syntactic sugar over JavaScript's prototype-based inheritance, but they offer a much more readable and structured way to create objects and manage inheritance. Modern JavaScript classes support constructors, instance and static methods, getters and setters, private fields, and inheritance. If you have experience with classes in Python, Java, or C++, JavaScript classes will feel familiar.

Class Basics

class User {
    // Private fields (ES2022)
    #password;

    constructor(name, email, password) {
        this.name = name;
        this.email = email;
        this.#password = password;
    }

    // Instance method
    greet() {
        return `Hello, I'm ${this.name}`;
    }

    // Getter
    get displayName() {
        return `${this.name} <${this.email}>`;
    }

    // Setter
    set fullName(value) {
        const [first, last] = value.split(" ");
        this.name = `${first} ${last}`;
    }

    // Private method
    #hashPassword(password) {
        return `hashed_${password}`;
    }

    // Static method
    static fromJSON(json) {
        const data = JSON.parse(json);
        return new User(data.name, data.email, data.password);
    }

    // Static property
    static MAX_NAME_LENGTH = 50;
}

const user = new User("Alice", "alice@example.com", "secret123");
console.log(user.greet());        // "Hello, I'm Alice"
console.log(user.displayName);    // "Alice "
user.fullName = "Alice Johnson";
// console.log(user.#password);   // SyntaxError — private

Inheritance

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

    speak() {
        return `${this.name} makes a sound`;
    }

    toString() {
        return `[Animal: ${this.name}]`;
    }
}

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

    speak() {
        return `${this.name} barks!`;  // Override parent method
    }

    fetch(item) {
        return `${this.name} fetches the ${item}`;
    }
}

class Cat extends Animal {
    speak() {
        return `${this.name} meows!`;
    }
}

const dog = new Dog("Buddy", "Lab");
const cat = new Cat("Whiskers");
console.log(dog.speak());   // "Buddy barks!"
console.log(cat.speak());   // "Whiskers meows!"
console.log(dog.fetch("ball"));  // "Buddy fetches the ball"

// instanceof checks
console.log(dog instanceof Dog);     // true
console.log(dog instanceof Animal);  // true

Practical Class Example

class TodoList {
    #items = [];
    #nextId = 1;

    add(title, priority = "medium") {
        const item = {
            id: this.#nextId++,
            title,
            priority,
            done: false,
            createdAt: new Date()
        };
        this.#items.push(item);
        return item;
    }

    toggle(id) {
        const item = this.#items.find(i => i.id === id);
        if (!item) throw new Error(`Item ${id} not found`);
        item.done = !item.done;
        return item;
    }

    remove(id) {
        const index = this.#items.findIndex(i => i.id === id);
        if (index === -1) throw new Error(`Item ${id} not found`);
        return this.#items.splice(index, 1)[0];
    }

    get pending() {
        return this.#items.filter(i => !i.done);
    }

    get completed() {
        return this.#items.filter(i => i.done);
    }

    get count() {
        return this.#items.length;
    }

    toJSON() {
        return JSON.stringify(this.#items, null, 2);
    }
}

const todos = new TodoList();
todos.add("Learn JavaScript", "high");
todos.add("Build a project", "high");
todos.add("Write tests", "medium");
todos.toggle(1);

console.log(`Pending: ${todos.pending.length}`);
console.log(`Completed: ${todos.completed.length}`);

Mixins Pattern

// JavaScript only supports single inheritance
// Use mixins for multiple behavior composition

const Serializable = (Base) => class extends Base {
    toJSON() {
        return JSON.stringify(this);
    }
    static fromJSON(json) {
        return Object.assign(new this(), JSON.parse(json));
    }
};

const Validatable = (Base) => class extends Base {
    validate() {
        for (const [key, value] of Object.entries(this)) {
            if (value === null || value === undefined) {
                throw new Error(`${key} is required`);
            }
        }
        return true;
    }
};

class Product extends Serializable(Validatable(Object)) {
    constructor(name, price) {
        super();
        this.name = name;
        this.price = price;
    }
}

const product = new Product("Laptop", 999);
product.validate();
console.log(product.toJSON());
Pro tip: Use private fields (#) for data that should not be accessed outside the class. Unlike the underscore convention (_private), # fields are truly private and enforced by the JavaScript engine. Use getters for computed or read-only properties and setters for validation on assignment.

Key Takeaways

  • ES6 classes provide clean syntax for OOP with constructors, methods, getters, setters, and static members.
  • Use # prefix for truly private fields and methods (enforced by the engine, not just convention).
  • Inheritance uses extends and super() to call parent constructors and methods.
  • Use mixins (higher-order class functions) to compose multiple behaviors since JavaScript only supports single inheritance.
  • Classes are syntactic sugar over prototypes — understanding prototypes helps when debugging.