Lesson 14: Classes and Object-Oriented Programming

What You'll Learn

  • What classes are and why they're useful
  • Creating classes with properties and methods
  • Constructors
  • Private fields (# syntax)
  • Inheritance and extending classes
  • Static members
  • Getters and setters

Why This Matters

Classes are blueprints for creating objects. They help you organize code, model real-world concepts, and reuse logic across your application. Object-oriented programming (OOP) is a fundamental paradigm used in countless applications.

---

Part 1: What is a Class?

A class is a template for creating objects with shared properties and methods.

Think of it like:

  • A blueprint for a house (class) → Many houses built from it (objects/instances)
  • A cookie cutter (class) → Many cookies made from it (objects)
  • A Car class → Many car instances (Honda, Toyota, etc.)
Create a new file: lesson-14/classes.js

---

Part 2: Creating Your First Class

class Person {
  // Properties
  name;
  age;
  
  // Constructor - runs when creating new instance
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // Method
  greet() {
    console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old.`);
  }
}

// Create instances (objects) from the class
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);

alice.greet();  // Hello, I'm Alice and I'm 30 years old.
bob.greet();    // Hello, I'm Bob and I'm 25 years old.

console.log(alice.name);  // Alice
console.log(bob.age);     // 25

Understanding the Parts

  • class Person: Defines the class
  • name, age: Properties (data)
  • constructor: Special method that runs when creating an instance
  • this: Refers to the current instance
  • greet(): Method (behavior)
  • new Person(): Creates a new instance

---

Part 3: Access Modifiers (Encapsulation)

In JavaScript, we can use naming conventions and closures to control access to class members. Modern JavaScript also supports private fields using the # prefix.

Public Properties (Default)

class Car {
  // All properties are public by default
  constructor(brand) {
    this.brand = brand;
  }
}

const car = new Car("Toyota");
console.log(car.brand);  // ✅ Works
car.brand = "Honda";     // ✅ Can modify

Private Fields (Using #)

Modern JavaScript supports truly private fields using the # prefix:

class BankAccount {
  #balance;  // Private field (only accessible inside class)
  
  constructor(initialBalance) {
    this.#balance = initialBalance;
  }
  
  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }
  
  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());  // 1500

// console.log(account.#balance);  // ❌ Syntax Error: Private field
// account.#balance = 10000;       // ❌ Syntax Error: Cannot access
Why use private fields?
  • Encapsulation: Hide implementation details
  • Control: Force users to use your methods
  • Safety: Prevent invalid states

Convention: Underscore for "Private"

Before the # syntax, developers used underscore as a convention:

class OldStylePrivate {
  constructor(secret) {
    this._secret = secret;  // Convention: underscore means "don't touch"
  }
  
  getSecret() {
    return this._secret;
  }
}

// Note: This is just a convention, not enforced by JavaScript

---

Part 4: Methods

Methods are functions inside classes:

class Calculator {
  add(a, b) {
    return a + b;
  }
  
  subtract(a, b) {
    return a - b;
  }
  
  multiply(a, b) {
    return a * b;
  }
  
  divide(a, b) {
    if (b === 0) {
      throw new Error("Cannot divide by zero");
    }
    return a / b;
  }
}

const calc = new Calculator();
console.log(calc.add(10, 5));       // 15
console.log(calc.multiply(4, 3));   // 12
console.log(calc.divide(20, 4));    // 5

---

Part 5: Getters and Setters

Special methods for controlled access to properties:

class User {
  #email;
  #age;
  
  constructor(email, age) {
    this.#email = email;
    this.#age = age;
  }
  
  // Getter - access like a property
  get email() {
    return this.#email;
  }
  
  // Setter - set like a property with validation
  set email(value) {
    if (!value.includes("@")) {
      throw new Error("Invalid email");
    }
    this.#email = value;
  }
  
  get age() {
    return this.#age;
  }
  
  set age(value) {
    if (value < 0 || value > 150) {
      throw new Error("Invalid age");
    }
    this._age = value;
  }
}

const user = new User("alice@example.com", 30);

// Use like properties (but actually calling methods)
console.log(user.email);  // alice@example.com
user.email = "newemail@example.com";  // ✅ Valid

// user.email = "invalid";  // ❌ Error: Invalid email
// user.age = 200;          // ❌ Error: Invalid age

---

Part 6: Static Members

Static members belong to the class itself, not instances:

class MathUtils {
  static PI = 3.14159;
  
  static add(a, b) {
    return a + b;
  }
  
  static max(a, b) {
    return a > b ? a : b;
  }
  
  static circleArea(radius) {
    return this.PI  radius  radius;
  }
}

// Use without creating instance
console.log(MathUtils.PI);             // 3.14159
console.log(MathUtils.add(5, 3));      // 8
console.log(MathUtils.max(10, 20));    // 20
console.log(MathUtils.circleArea(5));  // 78.53975

// const utils = new MathUtils();  // Usually not needed
When to use static:
  • Utility functions that don't need instance data
  • Constants
  • Factory methods

---

Part 7: Inheritance

Classes can extend other classes:

// Base class (parent)
class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  makeSound() {
    console.log("Some generic sound");
  }
  
  getInfo() {
    return `${this.name} is ${this.age} years old`;
  }
}

// Derived class (child)
class Dog extends Animal {
  constructor(name, age, breed) {
    super(name, age);  // Call parent constructor
    this.breed = breed;
  }
  
  // Override parent method
  makeSound() {
    console.log("Woof! Woof!");
  }
  
  // Add new method
  fetch() {
    console.log(`${this.name} is fetching the ball`);
  }
}

class Cat extends Animal {
  makeSound() {
    console.log("Meow!");
  }
  
  scratch() {
    console.log(`${this.name} is scratching`);
  }
}

const dog = new Dog("Rex", 5, "Golden Retriever");
dog.makeSound();  // Woof! Woof!
dog.fetch();      // Rex is fetching the ball
console.log(dog.getInfo());  // Rex is 5 years old

const cat = new Cat("Whiskers", 3);
cat.makeSound();  // Meow!
cat.scratch();    // Whiskers is scratching

The super Keyword

  • In constructor: super() calls parent constructor (must be first)
  • In methods: super.method() calls parent method
class Employee extends Person {
  constructor(name, age, employeeId) {
    super(name, age);  // Must call parent constructor
    this.employeeId = employeeId;
  }
  
  greet() {
    super.greet();  // Call parent greet
    console.log(`My employee ID is ${this.employeeId}`);
  }
}

---

Part 8: Practical Example - Task Manager

// Status constants (instead of enum)
const TaskStatus = {
  TODO: "TODO",
  IN_PROGRESS: "IN_PROGRESS",
  DONE: "DONE"
};

class Task {
  static nextId = 1;
  
  constructor(title, description) {
    this.id = Task.nextId++;
    this.title = title;
    this.description = description;
    this._status = TaskStatus.TODO;
  }
  
  get status() {
    return this._status;
  }
  
  start() {
    if (this._status === TaskStatus.TODO) {
      this._status = TaskStatus.IN_PROGRESS;
      console.log(`Task "${this.title}" started`);
    } else {
      console.log(`Cannot start task in ${this._status} status`);
    }
  }
  
  complete() {
    if (this._status === TaskStatus.IN_PROGRESS) {
      this._status = TaskStatus.DONE;
      console.log(`Task "${this.title}" completed!`);
    } else {
      console.log(`Cannot complete task in ${this._status} status`);
    }
  }
  
  getInfo() {
    return `[${this.id}] ${this.title} - ${this._status}`;
  }
}

class TaskManager {
  constructor() {
    this.tasks = [];
  }
  
  addTask(title, description) {
    const task = new Task(title, description);
    this.tasks.push(task);
    console.log(`Task added: ${task.getInfo()}`);
    return task;
  }
  
  getTask(id) {
    return this.tasks.find(task => task.id === id);
  }
  
  listTasks(status) {
    const filtered = status
      ? this.tasks.filter(task => task.status === status)
      : this.tasks;
    
    console.log("\n=== Tasks ===");
    filtered.forEach(task => console.log(task.getInfo()));
  }
  
  getTodoTasks() {
    return this.tasks.filter(task => task.status === TaskStatus.TODO);
  }
  
  getCompletedCount() {
    return this.tasks.filter(task => task.status === TaskStatus.DONE).length;
  }
}

// Usage
const manager = new TaskManager();

const task1 = manager.addTask("Learn JavaScript", "Complete lesson 14");
const task2 = manager.addTask("Build project", "Create a CLI app");

manager.listTasks();

task1.start();
task1.complete();

manager.listTasks();

console.log(`\nCompleted tasks: ${manager.getCompletedCount()}`);

---

Practice Exercises

Exercise 1: Bank Account System

Create a bank account class with:

  • Private balance
  • Methods: deposit, withdraw, transfer
  • Transaction history

Exercise 2: Library System

Create classes for:

  • Book (with ISBN, title, author)
  • Library (manages books)
  • Member (can borrow books)

Exercise 3: Game Characters

Create a base Character class and specific classes:

  • Warrior (high health, melee attacks)
  • Mage (low health, magic attacks)
  • Archer (medium health, ranged attacks)

---

Key Concepts Summary

Concept Purpose Example
Class Blueprint for objects class Person {}
Constructor Initialize instance constructor(name) {}
Private (#) Only accessible in class #balance
static Belongs to class static count = 0
extends Inheritance class Dog extends Animal
super Call parent class super(name)

What You Learned

  • ✅ How to create classes with properties and methods
  • ✅ How to use constructors
  • ✅ Private fields using # syntax
  • ✅ Getters and setters for controlled access
  • ✅ Static members for class-level data
  • ✅ Inheritance and method overriding
  • ✅ Object-oriented design principles

What's Next?

You've completed the core JavaScript course! You now have the foundation to build your own applications. Consider exploring:

  • Working with the DOM (Document Object Model) for web development
  • Building Node.js applications and APIs
  • Learning popular frameworks like React or Express