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.)
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