Object-Oriented Programming
Master Classes, Objects, Encapsulation, Inheritance, Polymorphism, Abstraction, Constructors, Method Overriding, Static Methods, super, Composition, and Advanced OOP concepts
Classes - The Blueprint ๐
Learn how Classes are like blueprints for creating objects
The Lego Blueprint Story
Imagine you want to build a LEGO castle. Instead of making up the design as you go, you follow a blueprint - a detailed plan that shows exactly what pieces you need and how they fit together.
In programming, a Class is like that LEGO blueprint! It's a plan that describes what an object will have (properties) and what it can do (methods). You can use the same blueprint to build many castles!
Cookie Cutter Analogy
A cookie cutter is a shape that helps you make many identical cookies. The Class is the cookie cutter shape, and each Object is a cookie you make with it. All cookies have the same shape, but you can decorate them differently!
Class Blueprint Diagram
// Defining a Car class (the blueprint)
class Car {
// Constructor - runs when we create a new car
constructor(brand, color) {
this.brand = brand; // The car's brand
this.color = color; // The car's color
this.speed = 0; // Speed starts at 0
this.fuelLevel = 100; // Full tank!
}
// Method - what the car can do
startEngine() {
console.log(`๐ฅ ${this.brand} engine started! Vroom vroom!`);
}
accelerate() {
this.speed += 10;
console.log(`๐๏ธ ${this.brand} speeding up! Current speed: ${this.speed} km/h`);
}
brake() {
this.speed = 0;
console.log(`๐ ${this.brand} stopped safely!`);
}
honk() {
console.log(`๐ข ${this.brand} says: Beep beep!`);
}
}
Key Points About Classes
- A Class is a blueprint/template for creating objects
- It defines properties (data/characteristics) the object will have
- It defines methods (actions) the object can perform
- You can create many objects from one class
- Think of it as a "mold" or "template" for making things
Objects - Real Things Made from Blueprints ๐
Create actual instances from your Class blueprints
The Factory Story
A car factory uses the same blueprint to make hundreds of cars. Each car is an Object - a real, actual car that drives on the road. They all share the same design, but each is its own unique car with its own color, license plate, and driver.
Class = Blueprint
Object = The actual car you can drive!
Video Game Characters
In a video game, there's one "Player" class, but you and your friend each have your own player object. You both can jump, run, and collect coins, but you might have different scores, different outfits, and be in different places!
Creating Objects from the Car Class
myTesla
brand: "Tesla"
color: "Red"
speed: 0
fuel: 100
myFord
brand: "Ford"
color: "Blue"
speed: 0
fuel: 100
myBMW
brand: "BMW"
color: "Black"
speed: 0
fuel: 100
// Creating objects from the Car class (using the blueprint)
let myTesla = new Car("Tesla", "Red");
let myFord = new Car("Ford", "Blue");
let myBMW = new Car("BMW", "Black");
// Each object is independent!
console.log(myTesla.color); // "Red"
console.log(myFord.color); // "Blue"
// Using methods on each object
myTesla.startEngine(); // ๐ฅ Tesla engine started!
myTesla.accelerate(); // ๐๏ธ Tesla speeding up! Current speed: 10 km/h
myTesla.accelerate(); // ๐๏ธ Tesla speeding up! Current speed: 20 km/h
myFord.startEngine(); // ๐ฅ Ford engine started!
myFord.honk(); // ๐ข Ford says: Beep beep!
// They don't affect each other!
console.log(myTesla.speed); // 20 (Tesla is fast!)
console.log(myFord.speed); // 0 (Ford hasn't moved yet)
// Creating a game character example
class Player {
constructor(name, character) {
this.name = name;
this.character = character;
this.score = 0;
this.lives = 3;
}
jump() {
console.log(`๐ฆ ${this.name}'s ${this.character} jumped!`);
}
collectCoin() {
this.score += 100;
console.log(`๐ฐ ${this.name} got a coin! Score: ${this.score}`);
}
}
let player1 = new Player("Alex", "Mario");
let player2 = new Player("Sam", "Luigi");
player1.collectCoin(); // ๐ฐ Alex got a coin! Score: 100
player2.collectCoin(); // ๐ฐ Sam got a coin! Score: 100
player1.collectCoin(); // ๐ฐ Alex got a coin! Score: 200
// Alex has 200, Sam has 100 - they're separate!
Object Essentials
- An Object is a real instance created from a Class
- Use
new ClassName()to create one - Each object has its own data, independent of others
- Objects share the same methods but have different property values
Encapsulation - The Protective Capsule ๐ก๏ธ
Keep data safe by bundling it with the methods that use it
The Medicine Capsule
Medicine comes in capsules that protect the important ingredients inside. You can't just grab the medicine powder - you swallow the capsule, and it safely delivers the medicine where it needs to go. The capsule hides and protects the important stuff inside.
Encapsulation does the same for data! It bundles data and methods together, and protects the data from outside interference.
The Piggy Bank Analogy
A piggy bank encapsulates your money: You can't just take money out directly. Instead, there are controlled ways to interact with it - a slot to add money, and a special opening (maybe you need to break it!) to get money out.
In OOP, we don't let outside code mess with our data directly. We provide safe "slots" (methods) to change data properly.
Encapsulation in Action
BankAccount
// BankAccount with encapsulated (protected) data
class BankAccount {
// The # makes these properties private (protected)
#balance;
#accountNumber;
#pin;
constructor(accountNumber, initialBalance, pin) {
this.#accountNumber = accountNumber;
this.#balance = initialBalance;
this.#pin = pin;
}
// Public method to check balance (controlled access)
getBalance(enteredPin) {
if (enteredPin === this.#pin) {
return this.#balance;
} else {
return "โ Incorrect PIN!";
}
}
// Public method to deposit money (controlled way to add)
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`๐ฐ Deposited $${amount}. New balance: $${this.#balance}`);
} else {
console.log("โ Cannot deposit negative amount!");
}
}
// Public method to withdraw (controlled way to remove)
withdraw(amount, enteredPin) {
if (enteredPin !== this.#pin) {
console.log("โ Incorrect PIN!");
return;
}
if (amount > this.#balance) {
console.log("โ Insufficient funds!");
} else if (amount > 0) {
this.#balance -= amount;
console.log(`๐ธ Withdrew $${amount}. Remaining: $${this.#balance}`);
}
}
}
// Using the encapsulated account
let myAccount = new BankAccount("ACC123", 1000, "1234");
// Can't access private data directly!
// myAccount.#balance = 999999; // โ ERROR!
// Must use controlled methods
myAccount.deposit(500); // โ
Works! Balance: 1500
myAccount.withdraw(200, "1234"); // โ
Works! Remaining: 1300
myAccount.withdraw(200, "0000"); // โ Incorrect PIN!
console.log(myAccount.getBalance("1234")); // ๐ต 1300
Encapsulation Benefits
- Protection: Data can't be changed accidentally or maliciously
- Control: You decide HOW data can be changed (validation, security)
- Organization: Data and methods stay together like a capsule
- Simpler Interface: Outside code doesn't need to know internal details
Inheritance - Passing Down Traits ๐จโ๐ฉโ๐งโ๐ฆ
Create new classes based on existing ones, like family traits
Family Traits
You inherit traits from your parents - maybe your mom's eye color, your dad's smile, your grandmother's curly hair. But you're also unique - you have your own personality, hobbies, and style! You got the "base model" from family but customized it.
In programming, a new class can inherit from a parent class, getting all its features while adding its own special ones!
Animal Kingdom
All dogs are animals - they inherit "animal things" like eating and sleeping. But dogs also have special dog things like barking and wagging tails. A Golden Retriever inherits from Dog, which inherits from Animal!
Inheritance Hierarchy
// Parent Class (The base blueprint)
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log(`๐ฝ๏ธ ${this.name} is eating.`);
}
sleep() {
console.log(`๐ด ${this.name} is sleeping.`);
}
}
// Child Class (inherits from Animal)
class Dog extends Animal {
constructor(name, age, breed) {
// Call parent's constructor
super(name, age);
this.breed = breed; // New property just for dogs
}
// New method just for dogs
bark() {
console.log(`๐ ${this.name} says: Woof woof!`);
}
// New method just for dogs
wagTail() {
console.log(`๐ ${this.name} is wagging tail happily!`);
}
}
// Grandchild Class (inherits from Dog)
class GoldenRetriever extends Dog {
constructor(name, age) {
// Call Dog's constructor
super(name, age, "Golden Retriever");
this.furColor = "golden";
}
// Special method just for Golden Retrievers
fetch() {
console.log(`๐พ ${this.name} fetched the ball! Good dog!`);
}
// Override parent's method (customize it)
bark() {
console.log(`๐ ${this.name} says: Woof! (so friendly!)`);
}
}
// Using the classes
let genericAnimal = new Animal("SomeAnimal", 5);
let myDog = new Dog("Rex", 3, "German Shepherd");
let myGolden = new GoldenRetriever("Buddy", 2);
// Animal can only do Animal things
genericAnimal.eat(); // โ
Works!
// genericAnimal.bark(); // โ Error! Animals don't bark
// Dog can do Animal things AND Dog things
myDog.eat(); // โ
Inherited from Animal
myDog.bark(); // โ
Dog's own method
// Golden Retriever can do everything!
myGolden.eat(); // โ
From Animal
myGolden.bark(); // โ
From Dog (customized)
myGolden.fetch(); // โ
Its own special method
Inheritance Power
- Reuses code: Child classes get all parent's features automatically
- Adds features: Children can have their own unique properties and methods
- Customizes: Children can override parent methods to change behavior
- Organizes: Creates a family tree of related classes
Polymorphism - Many Shapes, One Interface ๐ญ
Different objects can respond to the same message in their own way
The "Make Sound" Command
Imagine you have a remote control with one button: "Make Sound". When you press it at a dog, it barks. At a cat, it meows. At a bird, it chirps. Same command, but each animal responds differently!
That's polymorphism - many forms! Different objects respond to the same method call in their own special way.
Draw Command
In a drawing program, you have a "Draw" button. Click it on a circle, and you see a circle. Click it on a square, and you see a square. Click it on a triangle, and you see a triangle. Same "Draw" command, different results!
Polymorphism in Action
โฌ๏ธ
"Woof!"
โฌ๏ธ
"Meow!"
โฌ๏ธ
"Moo!"
โฌ๏ธ
"Oink!"
makeSound() - Different results!
// Parent class with a generic method
class Animal {
constructor(name) {
this.name = name;
}
// Generic version (can be overridden)
makeSound() {
console.log(`๐ ${this.name} makes a sound.`);
}
move() {
console.log(`๐พ ${this.name} moves.`);
}
}
// Each child customizes the behavior
class Dog extends Animal {
makeSound() {
console.log(`๐ ${this.name} barks: Woof! Woof!`);
}
move() {
console.log(`๐ ${this.name} runs on four legs.`);
}
}
class Cat extends Animal {
makeSound() {
console.log(`๐ฑ ${this.name} meows: Meow~`);
}
move() {
console.log(`๐ฑ ${this.name} sneaks quietly.`);
}
}
class Bird extends Animal {
makeSound() {
console.log(`๐ฆ ${this.name} chirps: Tweet tweet!`);
}
move() {
console.log(`๐ฆ ${this.name} flies in the sky!`);
}
}
// The power of polymorphism!
let animals = [
new Dog("Rex"),
new Cat("Whiskers"),
new Bird("Tweety")
];
console.log("=== Animal Concert ===");
// Same code works for ALL animals!
for (let animal of animals) {
animal.makeSound(); // Each responds differently!
}
console.log("\n=== Animal Race ===");
for (let animal of animals) {
animal.move(); // Each moves differently!
}
// Output:
// === Animal Concert ===
// ๐ Rex barks: Woof! Woof!
// ๐ฑ Whiskers meows: Meow~
// ๐ฆ Tweety chirps: Tweet tweet!
//
// === Animal Race ===
// ๐ Rex runs on four legs.
// ๐ฑ Whiskers sneaks quietly.
// ๐ฆ Tweety flies in the sky!
Why Polymorphism is Magical
- One interface, many implementations: Write code that works with ANY type of object
- Easy to extend: Add new animal types without changing the concert code!
- Clean code: One loop handles all animals, no matter how many types you add
- Natural behavior: Each object acts according to its true nature
Abstraction - Hiding Complexity ๐ญ
Show only what's needed, hide the complicated details
The Car Dashboard Story
When you drive a car, you see a simple dashboard: speedometer, fuel gauge, and buttons. You don't see the complex engine, fuel injection system, or transmission gears. The car abstracts all that complexity and shows you only what you need!
You just press the gas pedal - you don't need to know HOW the engine works. The complicated stuff is hidden, the simple interface is shown.
TV Remote Control
Your TV remote has simple buttons: Power, Volume Up, Channel. Behind the scenes, there's complex circuitry, signals, and processing. But you don't need to know any of that! The remote abstracts the TV's complexity.
Abstraction in Action
๐ง Complex Inner Workings
โข Fuel injection
โข Gear ratios
โข Temperature sensors
โข 1000+ moving parts
๐ค Simple Interface
Press brake to stop!
// Complex inner workings (HIDDEN - private)
class CoffeeMachine {
// Private complex stuff users don't need to see
#waterTemperature = 0;
#pressure = 0;
#beanGrinderStatus = "off";
constructor() {
this.#waterTemperature = 90; // Celsius
this.#pressure = 9; // Bars
}
// Private methods - internal complexity
#heatWater() {
console.log("๐ก๏ธ Heating water to " + this.#waterTemperature + "ยฐC...");
}
#grindBeans() {
console.log("โ Grinding beans...");
}
#applyPressure() {
console.log("๐จ Applying " + this.#pressure + " bars of pressure...");
}
// PUBLIC simple interface - this is what users see!
makeEspresso() {
console.log("โณ Making your espresso...");
this.#heatWater();
this.#grindBeans();
this.#applyPressure();
console.log("โ
Here's your espresso! โ");
}
makeLatte() {
console.log("โณ Making your latte...");
this.#heatWater();
this.#grindBeans();
this.#applyPressure();
this.#steamMilk();
console.log("โ
Here's your latte! ๐ฅโ");
}
steamMilk() {
console.log("๐ฅ Steaming milk...");
}
}
// Using the machine - SO SIMPLE!
let myMachine = new CoffeeMachine();
// Just press a button - no need to understand complex engineering!
myMachine.makeEspresso(); // Easy!
myMachine.makeLatte(); // Easy!
// Can't access private stuff (and shouldn't need to!)
// myMachine.#heatWater(); // โ Error - not accessible
// myMachine.#pressure; // โ Error - hidden data
Abstraction Benefits
- Hides complexity: Users see simple interface, not messy internals
- Easier to use: Just press a button, no engineering degree needed!
- Reduces errors: Can't mess with internal parts accidentally
- Focus on what matters: Use features without understanding implementation
Constructors & this - Object Creation ๐๏ธ
Special methods that create and initialize new objects
The Factory Robot Story
Imagine a factory robot that assembles toys. When you order a toy, the robot automatically constructs it: it adds wheels, paints it, installs batteries, and packages it. You don't do each step - the robot handles it all!
A constructor is like that robot - it automatically sets up a new object when it's created!
The "this" Keyword
When a person says "I" or "me", they're talking about themselves.
In programming, this is like "I" - it refers to the current object.
If a car says this.color, it means ITS OWN color!
class VideoGameCharacter {
// Constructor - runs automatically when "new" is called
constructor(name, characterClass, color) {
// "this" refers to the NEW object being created
this.name = name; // "My name is..."
this.class = characterClass; // "I am a warrior"
this.color = color; // "My color is red"
// Set default values
this.health = 100; // Everyone starts healthy
this.level = 1; // Everyone starts at level 1
this.inventory = []; // Empty backpack
console.log(`๐ฎ Created ${this.name} the ${this.class}!`);
}
jump() {
// "this" refers to whoever called the method
console.log(`๐ฆ ${this.name} jumped!`);
}
takeDamage(amount) {
this.health -= amount;
console.log(`๐ ${this.name} took ${amount} damage! Health: ${this.health}`);
if (this.health <= 0) {
console.log(`๐ ${this.name} has fallen!`);
}
}
levelUp() {
this.level++;
this.health = 100; // Restore health
console.log(`โญ ${this.name} leveled up to ${this.level}!`);
}
}
// Creating characters - constructor runs automatically!
let mario = new VideoGameCharacter("Mario", "Plumber", "Red");
// Output: ๐ฎ Created Mario the Plumber!
let luigi = new VideoGameCharacter("Luigi", "Plumber", "Green");
// Output: ๐ฎ Created Luigi the Plumber!
// Each has their own properties
console.log(mario.color); // "Red"
console.log(luigi.color); // "Green"
// Methods use "this" to know WHO they are
mario.jump(); // ๐ฆ Mario jumped!
luigi.jump(); // ๐ฆ Luigi jumped!
mario.takeDamage(30); // ๐ Mario took 30 damage! Health: 70
mario.levelUp(); // โญ Mario leveled up to 2!
Constructor & this Tips
- constructor() runs automatically when you use
new - this = "the current object" or "me/myself"
- Use this.property to set up initial values
- this in methods refers to whoever called the method
- Each object gets its own copy of properties
Method Overriding - Customizing Parent ๐
Child classes can change how inherited methods work
The Family Recipe Story
Your grandma has a cookie recipe. Your mom learned it, but she adds chocolate chips. You learned from your mom, but you add sprinkles on top! Same recipe, but each generation customizes it.
Method overriding is when a child class changes how a parent's method works!
Animal Sounds
The generic Animal class has makeSound() that says "Some sound".
But Dog overrides it to say "Woof!". Cat overrides it to say "Meow!".
Each animal customizes the behavior!
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
// Parent method - can be overridden
calculateBonus() {
return this.salary * 0.05; // 5% bonus
}
introduce() {
return `Hi, I'm ${this.name} and I earn $${this.salary}.`;
}
}
class Manager extends Employee {
constructor(name, salary, teamSize) {
super(name, salary);
this.teamSize = teamSize;
}
// OVERRIDE: Managers get bigger bonus!
calculateBonus() {
return this.salary * 0.10; // 10% bonus - different from parent!
}
// OVERRIDE: Custom introduction
introduce() {
return `Hi, I'm ${this.name}, a manager leading ${this.teamSize} people!`;
}
// NEW method only for managers
conductMeeting() {
console.log(`๐ ${this.name} is conducting a team meeting.`);
}
}
class Developer extends Employee {
constructor(name, salary, programmingLanguage) {
super(name, salary);
this.language = programmingLanguage;
}
// OVERRIDE: Developers get different bonus calculation
calculateBonus() {
let baseBonus = super.calculateBonus(); // Get parent's calculation
return baseBonus + 500; // Add extra $500 for coding skills!
}
// OVERRIDE: Custom introduction
introduce() {
return `Hi, I'm ${this.name}, a ${this.language} developer!`;
}
}
let regularEmployee = new Employee("Bob", 50000);
let manager = new Manager("Alice", 80000, 10);
let developer = new Developer("Charlie", 70000, "JavaScript");
// Same method name, different results!
console.log(regularEmployee.calculateBonus()); // 2500 (5% of 50000)
console.log(manager.calculateBonus()); // 8000 (10% of 80000)
console.log(developer.calculateBonus()); // 4000 (5% + 500)
// Custom introductions
console.log(regularEmployee.introduce()); // Hi, I'm Bob...
console.log(manager.introduce()); // Hi, I'm Alice, a manager...
console.log(developer.introduce()); // Hi, I'm Charlie, a JavaScript developer...
Overriding Rules
- Override = Child provides its own version of parent's method
- Method name must be exactly the same as parent's
- Use
super.method()to call parent's version too - Overriding enables polymorphism (different behaviors)
- Child can override some methods, inherit others unchanged
Static Methods - Class-Level Functions ๐
Methods that belong to the class, not individual objects
The Math Class Story
You don't need to create a "math object" to use Math.floor() or Math.random(). These functions belong to the class itself, not to any specific number!
Static methods are utility functions that belong to the blueprint, not to the things made from the blueprint.
Toolbox Analogy
A toolbox has tools anyone can use - you don't create a "toolbox object" for each screw you want to tighten. Static methods are like that toolbox - shared utilities for everyone!
Static vs Instance Methods
Instance Methods
Need an object to use them
Work with specific data
let car = new Car("BMW");
car.drive(); // โ Works!
Static Methods
Belong to the class itself
Utility/helper functions
Car.comparePrice(car1, car2);
// โ No object needed!
class Student {
constructor(name, grade) {
this.name = name;
this.grade = grade;
// Track total students created
Student.totalStudents++;
}
// Instance method - needs a specific student
study() {
console.log(`${this.name} is studying hard! ๐`);
}
// Instance method
getInfo() {
return `${this.name}: Grade ${this.grade}`;
}
// ============================================
// STATIC METHODS - Belong to the CLASS
// ============================================
// Static property (belongs to class, not objects)
static totalStudents = 0;
// Static method - utility function
static isValidGrade(grade) {
return grade >= 0 && grade <= 100;
}
// Static method - compare two students
static compareGrades(student1, student2) {
if (student1.grade > student2.grade) {
return `${student1.name} has better grades!`;
} else if (student2.grade > student1.grade) {
return `${student2.name} has better grades!`;
} else {
return "They're tied!";
}
}
// Static method - get statistics
static getTotalStudents() {
return `Total students: ${Student.totalStudents}`;
}
// Static method - factory method
static createHonorStudent(name) {
// Create a student with automatic A+ grade
return new Student(name, 100);
}
}
// Using INSTANCE methods (need objects)
let alice = new Student("Alice", 85);
let bob = new Student("Bob", 92);
alice.study(); // โ Instance method
console.log(alice.getInfo()); // โ Instance method
// Using STATIC methods (no object needed!)
console.log(Student.isValidGrade(95)); // โ true
console.log(Student.isValidGrade(150)); // โ false
console.log(Student.compareGrades(alice, bob)); // โ "Bob has better grades!"
console.log(Student.getTotalStudents()); // โ "Total students: 2"
// Factory method - creates object for you
let starStudent = Student.createHonorStudent("Charlie");
console.log(starStudent.getInfo()); // Charlie: Grade 100
When to Use Static
- Utility functions that don't need object data (Math.floor, Math.random)
- Factory methods that create objects in special ways
- Class-level data (count of all objects created)
- Helper functions related to the class concept
- Call with
ClassName.method()notobject.method()
super Keyword - Access Parent ๐
Call parent's constructor and methods from child class
The "Call Mom" Story
When you need advice, you might "call mom" even though you're an adult now. super is like that - it's how a child class "calls" its parent class to use the parent's constructor or methods!
When creating a specialized version, first let the parent do its setup, then add your customizations!
The Supermarket Chain
A local franchise store has the parent's "super" branding and products, but adds its own local specialties. It uses the parent foundation but customizes for its location!
How super Works
constructor(name) {
this.name = name;
}
constructor(name, age) {
super(name); // Parent sets name
this.age = age; // Child adds age
}
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.energy = 100;
}
introduce() {
return `Hi, I'm ${this.name}, ${this.age} years old.`;
}
rest() {
this.energy = 100;
console.log(`${this.name} is fully rested! โก`);
}
}
class Employee extends Person {
constructor(name, age, jobTitle, salary) {
// MUST call super() first before using this!
super(name, age); // Calls parent's constructor
// Now add employee-specific properties
this.jobTitle = jobTitle;
this.salary = salary;
}
// Override but also use parent's version
introduce() {
// Get parent's introduction
let parentIntro = super.introduce();
// Add our custom part
return `${parentIntro} I work as ${this.jobTitle}.`;
}
work() {
if (this.energy > 20) {
this.energy -= 20;
console.log(`${this.name} is working hard! ๐ผ`);
} else {
// Call parent's method when tired
console.log(`${this.name} is too tired to work...`);
super.rest(); // Use parent's rest method
}
}
}
let person = new Person("Grandma", 70);
let employee = new Employee("Alice", 30, "Developer", 80000);
// Both can use Person methods
console.log(person.introduce()); // "Hi, I'm Grandma..."
console.log(employee.introduce()); // "Hi, I'm Alice... I work as Developer."
// Employee can work (Person can't)
employee.work(); // Alice is working!
employee.work(); // Alice is working!
employee.work(); // Alice is working!
employee.work(); // Tired, automatically rests!
super Rules
-
super()calls parent's constructor in child's constructor -
super.method()calls parent's method version - Must call
super()before usingthisin child's constructor - Use super to extend (not replace) parent's behavior
- super = "the parent class's version"
Composition - Has-a Relationship ๐งฉ
Build complex objects by combining simpler ones
Car Parts Story
A car has an engine, has wheels, has seats. It doesn't inherit from engine (a car isn't an engine!), it contains an engine. This is composition - building complex things from simpler parts.
"Is-a" = Inheritance (Dog is an Animal)
"Has-a" = Composition (Car has an Engine)
Burger Composition
A burger isn't a bun or a patty - it's MADE OF them. The bun, patty, lettuce, and cheese are components that come together to make a burger. You can mix and match parts!
Composition vs Inheritance
Inheritance (Is-a)
Dog is an Animal
(Family relationship)
Composition (Has-a)
Car has an Engine
(Part relationship)
// Component classes - simple, reusable parts
class Engine {
constructor(horsepower) {
this.horsepower = horsepower;
this.isRunning = false;
}
start() {
this.isRunning = true;
console.log(`๐ฅ Engine started! (${this.horsepower} HP)`);
}
stop() {
this.isRunning = false;
console.log("๐ Engine stopped.");
}
}
class Wheel {
constructor(size) {
this.size = size;
}
rotate() {
console.log(`๐ ${this.size}" wheel spinning!`);
}
}
// Car class uses composition - it HAS these parts
class Car {
constructor(make, model, engineHP, wheelSize) {
this.make = make;
this.model = model;
// Composition: Car HAS an engine
this.engine = new Engine(engineHP);
// Composition: Car HAS wheels (array of objects)
this.wheels = [
new Wheel(wheelSize),
new Wheel(wheelSize),
new Wheel(wheelSize),
new Wheel(wheelSize)
];
}
start() {
console.log(`๐ Starting ${this.make} ${this.model}...`);
this.engine.start(); // Delegate to engine
}
drive() {
if (this.engine.isRunning) {
this.wheels.forEach(wheel => wheel.rotate()); // All wheels spin!
console.log("๐๐จ Car is moving!");
} else {
console.log("โ Start the engine first!");
}
}
}
// Build a car from parts!
let myCar = new Car("Toyota", "Camry", 200, 17);
// Car coordinates its components
myCar.start(); // Delegates to engine.start()
myCar.drive(); // Delegates to wheels
// You can swap parts! (Flexible!)
myCar.engine = new Engine(300); // Upgrade to stronger engine!
When to Use Composition
- Object has parts that can change independently
- Want to mix and match features (like building blocks)
- Relationship is "part of" not "type of"
- More flexible than inheritance - swap parts at runtime!
- "Favor composition over inheritance" - OOP principle
Advanced OOP Concepts
Master professional-level Object-Oriented Programming techniques
Access Modifiers
Security Levels for Your Data
The Museum Security Analogy
Imagine a science museum with different areas:
- ๐ Public areas: Anyone can enter - the main hall, gift shop
- ๐ Private labs: Only scientists can enter - restricted experiments
- ๐ก๏ธ Protected archives: Staff and their assistants can access - research materials
Different people have different access levels based on their roles!
Visual: Access Levels
Anyone can access
Class + Children
Only this class
Code Example: Bank Account Security
class BankAccount {
// ๐ Public - Anyone can see account type
accountType = "Savings";
// ๐ Private - Only BankAccount can access
#balance = 1000;
#pin = "1234";
// ๐ก๏ธ Protected - Class + subclasses can access
_transactionLog = [];
// ๐ Public method
getAccountType() {
return this.accountType; // โ
Allowed
}
// ๐ Private method
#validatePin(pin) {
return pin === this.#pin; // Only class can use
}
withdraw(amount, pin) {
// ๐ Private check inside public method
if (this.#validatePin(pin)) {
if (this.#balance >= amount) {
this.#balance -= amount;
return `๐ธ Withdrew $${amount}`;
}
}
return "โ Transaction failed";
}
}
const account = new BankAccount();
console.log(account.accountType); // โ
"Savings"
console.log(account.#balance); // โ ERROR! Private
console.log(account._transactionLog); // โ ๏ธ Not recommended
account.withdraw(100, "1234"); // โ
Works!
Key Points
- Public: Accessible from anywhere - like a public park
- Private: Only inside the class - like your bedroom
- Protected: Class and its children - like family secrets
- Use private for sensitive data (passwords, balance, secrets)
- Use protected when subclasses need access but outsiders don't
Getters & Setters
Controlled Property Access
The Bank Vault Analogy
Imagine a bank vault with a special system:
- Getting money: You ask the teller โ They verify your ID โ Give you money
- Setting money: You deposit โ Teller counts and validates โ Stores safely
You can't just walk in and grab money! There's a controlled process.
Visual: Controlled Access
Gatekeeper
Code Example: Smart Temperature Control
class SmartThermostat {
#temperature = 22; // Private - hidden
// ๐ Getter - Read with logic
get temperature() {
console.log("๐ Reading temperature...");
return `${this.#temperature}ยฐC`;
}
// โ๏ธ Setter - Write with validation
set temperature(value) {
if (value < 16) {
console.log("โ Too cold! Minimum is 16ยฐC");
this.#temperature = 16;
} else if (value > 30) {
console.log("โ Too hot! Maximum is 30ยฐC");
this.#temperature = 30;
} else {
console.log(`โ
Setting temperature to ${value}ยฐC`);
this.#temperature = value;
}
}
// Bonus: Computed property
get isComfortable() {
return this.#temperature >= 20 && this.#temperature <= 25;
}
}
const ac = new SmartThermostat();
// Using getter
console.log(ac.temperature); // "๐ Reading temperature... 22ยฐC"
// Using setter with validation
ac.temperature = 35; // โ "Too hot! Maximum is 30ยฐC"
ac.temperature = 10; // โ "Too cold! Minimum is 16ยฐC"
ac.temperature = 24; // โ
"Setting temperature to 24ยฐC"
console.log(ac.isComfortable); // true
Key Points
- Getters: Control how data is read (format, validate, calculate)
- Setters: Control how data is written (validation, logic, side effects)
- Properties can be read-only (getter only) or write-only (setter only)
- Computed properties derive values from other data
- Perfect for data validation and formatting!
Prototype Chain
JavaScript's Inheritance Engine
The Family Recipe Chain
Imagine a family recipe book passed down generations:
- Great-Grandma's book: Basic bread recipe
- Grandma's book: Inherits bread + adds her special sauce
- Mom's book: Inherits both + adds her pasta recipe
- Your book: Inherits all + adds your pizza recipe
When you cook, you can use recipes from any generation!
Visual: The Chain
(Top of chain)
Code Example: The Inheritance Chain
// Base prototype
const Animal = {
breathe() {
return "๐ซ Breathing...";
},
eat() {
return "๐ฝ๏ธ Eating...";
}
};
// Dog inherits from Animal
const Dog = Object.create(Animal);
Dog.bark = function() {
return "๐ Woof! Woof!";
};
Dog.fetch = function() {
return "๐พ Fetching ball!";
};
// Create a specific dog
const myDog = Object.create(Dog);
myDog.name = "Buddy";
// The prototype chain in action:
console.log(myDog.name); // ๐ "Buddy" (own property)
console.log(myDog.bark()); // ๐ "Woof! Woof!" (from Dog)
console.log(myDog.eat()); // ๐ฝ๏ธ "Eating..." (from Animal)
console.log(myDog.toString()); // ๐ฆ "[object Object]" (from Object.prototype)
// Check the chain
console.log(Object.getPrototypeOf(myDog) === Dog); // true
console.log(Object.getPrototypeOf(Dog) === Animal); // true
console.log(Object.getPrototypeOf(Animal) === Object.prototype); // true
Key Points
- Every object in JavaScript has a prototype (parent)
- When you access a property, JS walks up the chain until it finds it
Object.create()makes a new object with a specific prototype- Classes in modern JS are syntactic sugar over prototypes
- The chain ends at
Object.prototype(the ultimate ancestor)
Association & Aggregation
Loose Object Relationships
The Friend Circle Analogy
Imagine a friend group:
- Association: You know each other, can talk, but have separate lives
- Aggregation: A study group - members belong to the group but exist independently
- vs Composition: Your heart is PART of you - can't exist separately!
Loose relationships allow flexibility and independence!
Visual: Relationship Types
"Knows each other"
"Has members"
"Part of" (strong)
Code Example: University System
// ๐ ASSOCIATION: Professor knows Students (loose)
class Professor {
constructor(name) {
this.name = name;
this.students = []; // Can teach many students
}
addStudent(student) {
this.students.push(student);
// Student exists independently
}
}
// ๐งฉ AGGREGATION: Department has Professors
class Department {
constructor(name) {
this.name = name;
this.professors = []; // Professors exist outside dept
}
addProfessor(prof) {
this.professors.push(prof);
}
// Professors can leave, department still exists
removeProfessor(prof) {
const index = this.professors.indexOf(prof);
if (index > -1) this.professors.splice(index, 1);
}
}
// Students exist independently
const alice = { name: "Alice" };
const bob = { name: "Bob" };
// Professors exist independently
const profSmith = new Professor("Dr. Smith");
const profJones = new Professor("Dr. Jones");
// Association: Prof knows students
profSmith.addStudent(alice);
profSmith.addStudent(bob);
// Aggregation: Dept has professors
const csDept = new Department("Computer Science");
csDept.addProfessor(profSmith);
csDept.addProfessor(profJones);
// Prof can leave dept, but still exists
console.log(csDept.professors.length); // 2
csDept.removeProfessor(profSmith);
console.log(profSmith.name); // Still exists! "Dr. Smith"
Key Points
- Association: A "knows" relationship - objects are aware of each other
- Aggregation: A "has" relationship - weak ownership, parts can exist independently
- Composition: Strong ownership - parts can't exist without the whole
- Use aggregation when lifetime of parts should be independent
- Both are more flexible than inheritance!
Method Overloading
Same Name, Different Inputs
The Smart Assistant Analogy
Imagine a smart home assistant that responds to "Turn on lights":
- "Turn on lights" โ All house lights on
- "Turn on bedroom lights" โ Only bedroom lights
- "Turn on lights at 50%" โ Dimmable lights at 50%
- "Turn on lights at 7 PM" โ Schedule for 7 PM
Same command, but different parameters = different behavior!
Visual: One Method, Many Forms
radius
width, height
base, height
side
Code Example: Flexible Calculator
class Calculator {
// One method, multiple ways to call it
calculate(...args) {
const count = args.length;
if (count === 1 && typeof args[0] === 'number') {
// Square: side * side
return `๐ Square: ${args[0] * args[0]}`;
}
else if (count === 2) {
// Rectangle: width * height
return `โญ Rectangle: ${args[0] * args[1]}`;
}
else if (count === 3) {
// Triangle: 0.5 * base * height
return `๐ Triangle: ${0.5 * args[0] * args[1]}`;
}
else if (count === 0) {
return "โ Please provide dimensions!";
}
else {
return `โจ Sum of ${count} numbers: ${args.reduce((a,b) => a+b, 0)}`;
}
}
// Another example: greet with optional parameters
greet(name, greeting = "Hello", punctuation = "!") {
return `${greeting}, ${name}${punctuation}`;
}
}
const calc = new Calculator();
// Method "overloading" through parameter count/type
console.log(calc.calculate(5)); // ๐ Square: 25
console.log(calc.calculate(4, 6)); // โญ Rectangle: 24
console.log(calc.calculate(3, 4, 0)); // ๐ Triangle: 6
console.log(calc.calculate(1, 2, 3, 4)); // โจ Sum of 4 numbers: 10
// Optional parameters
console.log(calc.greet("Alice")); // "Hello, Alice!"
console.log(calc.greet("Bob", "Hi")); // "Hi, Bob!"
console.log(calc.greet("Carol", "Hey", ".")); // "Hey, Carol."
Key Points
- Method Overloading: Same method name, different parameter lists
- JavaScript doesn't have true overloading, but we simulate it with defaults and rest parameters
- Use parameter count, types, or default values to create flexible methods
- Makes APIs more intuitive - one name, many uses
- Document the different ways to call your methods clearly!
OOP Mastery Check
Test your understanding of all 16 OOP concepts: Classes, Objects, Encapsulation, Inheritance, Polymorphism, Abstraction, Constructors, Method Overriding, Static Methods, super, Composition, Access Modifiers, Getters/Setters, Prototype Chain, Association, and Method Overloading!