Skip to main content

Command Palette

Search for a command to run...

Understanding Object-Oriented Programming in JavaScript

One blueprint. Infinite heroes. That's what a class is.

Updated
•26 min read•View as Markdown
Understanding Object-Oriented Programming in JavaScript
J

Turning chai into code and ideas into full-stack applications. Sharing lessons from my development journey, one commit at a time.

Build Your Character — Object-Oriented Programming in JavaScript

One blueprint. Infinite heroes. That's what a class is.

What you'll learn in this blog:

  • What OOP is and why it exists

  • How a class is different from a plain object

  • How to write and read a constructor step by step

  • What this actually means and why it matters

  • How to add methods and call them on instances

  • How to create multiple objects from one class

  • What encapsulation is and how private fields work

  • Common beginner mistakes and how to avoid them

  • A full mini-game you build line by line as you read


You're standing at a forge deep inside a dungeon.

On the anvil in front of you sits a glowing scroll. It describes a hero — every stat, every move, every ability. But it isn't a hero. It's the instructions for building one.

You pick it up and read the first line:

class Character { ... }

That's a class. And once you understand it, you can forge any hero you want — as many as you need — each one alive, independent, and ready for battle.

🗺️ Before you start: This blog assumes you know what a JavaScript object is ({ name: "Aryan", age: 22 }), how functions work, and what const and let do. If those feel shaky, read Episode 02 first. Everything here builds on top of them.


🪜 The Problem We're Solving First

Let's say you're building a small game. You need three heroes. Here's what most beginners do:

// The beginner approach — three separate objects, written by hand
const hero1 = {
  name: "Aragorn",
  hp:   200,
  role: "warrior",
  attack: function() {
    console.log(this.name + " swings a sword!");
  }
};

const hero2 = {
  name: "Lyra",
  hp:   140,
  role: "mage",
  attack: function() {
    console.log(this.name + " swings a sword!"); // copy-pasted
  }
};

const hero3 = {
  name: "Kira",
  hp:   160,
  role: "rogue",
  attack: function() {
    console.log(this.name + " swings a sword!"); // copy-pasted again
  }
};

You copy-pasted the attack function three times. Now imagine:

  • You need to fix a bug in attack — you have to change it in three places

  • You need 50 heroes — you write 50 objects by hand

  • A new hero type needs a defend method — you add it 50 times

This is the exact problem OOP was invented to solve. Write the shape of a hero once. Stamp it out as many times as you need.

🧠 What is Object-Oriented Programming?

Before we touch any code, let's settle the concept.

Object-Oriented Programming (OOP) is a way of organising your code around things — objects that have properties and behaviours — rather than just writing one instruction after another.

Instead of asking "what steps should the program follow?" — OOP asks "what things exist in this world, and what can they do?"

The real world already works this way:

  • A car has properties (colour, speed, fuel) and behaviours (accelerate, brake, honk)

  • A bank account has properties (balance, owner) and behaviours (deposit, withdraw)

  • A game character has properties (name, health, level) and behaviours (attack, defend, level up)

OOP lets you model these things directly in code.

Why does it matter?

Without OOP, if you wanted three heroes, you'd write three separate objects by hand:

const hero1 = { name: "Aragorn", hp: 200, role: "warrior" };
const hero2 = { name: "Lyra",    hp: 140, role: "mage"    };
const hero3 = { name: "Kira",    hp: 160, role: "rogue"   };

Manageable for three. Painful for thirty. And if you want all heroes to have an attack() method? You'd add it to every single object manually.

OOP's solution: write the recipe once. Use it forever.


📜 The Blueprint: What is a Class?

A class is a blueprint — a template that defines what properties and methods every object of that type will have.

It's not an object itself. It's the instructions for building objects.

Think of it exactly like a character creation screen in an RPG:

  • The screen itself is the class

  • Each hero you create from it is an instance (an object)

  • Every hero follows the same template but has their own name, stats, and story

// The class — the blueprint
class Character {
  constructor(name, hp) {
    this.name = name;
    this.hp   = hp;
  }
}

// Three instances — three heroes, all from the same mould
const aragorn = new Character("Aragorn", 200);
const lyra    = new Character("Lyra",    140);
const kira    = new Character("Kira",    160);

Three calls to new. Three independent heroes. One class.

💡 Class = cookie cutter. Instance = cookie. You can make a hundred cookies from one cutter. Each cookie is real and separate — but they all came from the same shape.

The anatomy of a class — every part labelled

Before writing your first class, let's name every part so nothing surprises you:

class Character {           // 1. "class" keyword + class name (PascalCase by convention)
  //                        // 2. Opening brace — everything inside is the class body

  constructor(name, hp) {   // 3. The constructor method — always named "constructor"
    //         ↑     ↑      // 4. Parameters — what you pass when calling "new"
    this.name = name;       // 5. "this.name" — a property attached to this instance
    this.hp   = hp;         // 6. Another property
  }                         // 7. Constructor ends

  attack() {                // 8. A method — no "function" keyword needed inside class
    console.log("Strike!"); // 9. Method body
  }                         // 10. Method ends

}                           // 11. Class body ends

// Using it:
const hero = new Character("Aragorn", 200); // 12. "new" keyword + class name + arguments
//    ↑                                      // 13. The instance — stored in a variable

⚠️ Class names use PascalCase — first letter of each word capitalised. Character, BankAccount, StudentProfile. Variable names use camelCase. This is a convention, not a rule — but every JavaScript developer follows it.


🔨 The Forge: class and constructor()

The constructor is the most important method in a class. It runs once, automatically, the moment you call new. Its job: take the arguments you pass in and wire them up to the object using this.

class Character {
  constructor(name, hp) {
    //         ↑     ↑
    //   arguments passed to new Character(...)

    this.name = name;  // attach name to THIS object
    this.hp   = hp;    // attach hp   to THIS object
  }
}

What is this?

this refers to the specific object being created right now. When you call new Character("Aragorn", 200), JavaScript creates a fresh empty object, runs the constructor with that object as this, and returns it to you.

const aragorn = new Character("Aragorn", 200);
// Inside constructor: this = the new aragorn object
// this.name → aragorn.name = "Aragorn"
// this.hp   → aragorn.hp   = 200

console.log(aragorn.name); // "Aragorn"
console.log(aragorn.hp);   // 200

Every time you call new, a brand new this is created. Aragorn's this is not Lyra's this. They are completely separate objects.

const aragorn = new Character("Aragorn", 200);
const lyra    = new Character("Lyra", 140);

aragorn.hp = 50;          // Aragorn takes a hit
console.log(lyra.hp);     // 140 — Lyra is fine. Separate objects.

💡 this = the current instance. It's how the class refers to itself. Without this.name = name, the name variable would just disappear when the constructor finished. this makes it stick.

What new actually does — step by step

Beginners often treat new as magic. Here's exactly what happens when you write new Character("Aragorn", 200):

// When you write this:
const aragorn = new Character("Aragorn", 200);

// JavaScript does this internally (simplified):
// Step 1 — Creates a brand new empty object: {}
// Step 2 — Sets its prototype to Character.prototype
// Step 3 — Runs the constructor with "this" = that new empty object
//           → this.name = "Aragorn"  (the empty object gets a "name" property)
//           → this.hp   = 200        (the empty object gets an "hp" property)
// Step 4 — Returns the now-populated object to you
// Step 5 — You store it in "aragorn"

After new finishes, aragorn looks exactly like this:

{
  name: "Aragorn",
  hp: 200
}
// Plus access to all methods defined in the class

What happens if you forget new?

// Common beginner mistake — forgetting "new"
const hero = Character("Aragorn", 200);
// TypeError: Cannot set properties of undefined
// Because "this" inside the constructor is now undefined (or globalThis in sloppy mode)

// Always use "new"
const hero = new Character("Aragorn", 200);

Always use new. JavaScript won't remind you — it'll just break in confusing ways.

Default property values in the constructor

Sometimes you want a property to always start at a fixed value regardless of what's passed in:

class Character {
  constructor(name, hp) {
    this.name  = name;
    this.hp    = hp;
    this.alive = true;   // always starts true — not a parameter
    this.level = 1;      // always starts at 1
    this.xp    = 0;      // always starts at 0
  }
}

const hero = new Character("Aragorn", 200);
console.log(hero.alive); // true  — set by constructor, not by caller
console.log(hero.level); // 1
console.log(hero.xp);    // 0

⚔️ The Battle: Methods

A hero that can't do anything is just a statue. Methods are the actions — functions that live inside a class and are available on every instance.

class Character {
  constructor(name, hp) {
    this.name = name;
    this.hp   = hp;
  }

  // Methods live directly inside the class — no "function" keyword needed
  attack(target) {
    target.hp -= 50;
    console.log(`\({this.name} attacks! \){target.name} has ${target.hp} HP left.`);
  }

  heal() {
    this.hp += 30;
    console.log(`\({this.name} heals! HP is now \){this.hp}.`);
  }

  status() {
    console.log(`\({this.name} | HP: \){this.hp}`);
  }
}

Every instance gets these methods automatically — you define them once, they're available everywhere:

const aragorn = new Character("Aragorn", 200);
const goblin  = new Character("Goblin",  80);

aragorn.attack(goblin);   // "Aragorn attacks! Goblin has 30 HP left."
aragorn.heal();            // "Aragorn heals! HP is now 230."
goblin.status();           // "Goblin | HP: 30"

Methods vs plain functions

Without a class, you'd need to write attack separately for every character — or pass the character as an argument every time. Methods eliminate that. They know which object they belong to through this.

// Without OOP — repetitive, fragile
function attack(attacker, target) {
  target.hp -= 50;
}
attack(aragorn, goblin);

// With OOP — clean, self-contained
aragorn.attack(goblin);

Methods can call other methods using this

This is something beginners often miss — methods can call each other inside the class using this.methodName():

class Character {
  constructor(name, hp) {
    this.name  = name;
    this.hp    = hp;
    this.alive = true;
  }

  takeDamage(amount) {
    this.hp -= amount;
    this.checkAlive(); // calling another method from inside a method
  }

  checkAlive() {       // this method is called by takeDamage — not directly by the user
    if (this.hp <= 0) {
      this.alive = false;
      console.log(`${this.name} has fallen.`);
    }
  }

  status() {
    const state = this.alive ? "alive" : "fallen";
    return `\({this.name} | HP: \){this.hp} | Status: ${state}`;
  }
}

const goblin = new Character("Goblin", 80);
goblin.takeDamage(90);      // internally calls checkAlive()
console.log(goblin.status()); // "Goblin | HP: -10 | Status: fallen"

Methods can return values

Not every method needs to console.log. Methods that return a value are more flexible — the caller decides what to do with the result:

class Character {
  constructor(name, hp) {
    this.name = name;
    this.hp   = hp;
  }

  // Returns a value — caller decides what to do with it
  isAlive() {
    return this.hp > 0;  // returns true or false
  }

  getStatus() {
    return `\({this.name}: \){this.hp} HP`;  // returns a string
  }
}

const hero = new Character("Aragorn", 200);

// Store the return value
const alive = hero.isAlive();
console.log(alive);           // true

// Use it in a condition
if (hero.isAlive()) {
  console.log("The battle continues!");
}

// Use it inline
console.log(hero.getStatus()); // "Aragorn: 200 HP"

🧑‍🤝‍🧑 Summoning: Creating Multiple Instances

This is where the blueprint pays off. Write the class once. Summon as many instances as you need.

class Character {
  constructor(name, hp, role) {
    this.name  = name;
    this.hp    = hp;
    this.role  = role;
    this.alive = true;
  }

  attack(target) {
    target.hp -= 50;
  }

  introduce() {
    return `I am \({this.name}, a \){this.role} with ${this.hp} HP.`;
  }
}

// Summon three heroes from one class
const aragorn = new Character("Aragorn", 200, "warrior");
const lyra    = new Character("Lyra",    140, "mage");
const kira    = new Character("Kira",    160, "rogue");

// Each has the same methods — but their own data
console.log(aragorn.introduce()); // "I am Aragorn, a warrior with 200 HP."
console.log(lyra.introduce());    // "I am Lyra, a mage with 140 HP."

// Changing one doesn't affect the others
aragorn.hp = 50;
console.log(lyra.hp); // 140 — untouched

Code reusability — the whole point

Notice what you didn't write:

  • You didn't define introduce() three separate times

  • You didn't manually copy the same structure three times

  • You didn't worry about inconsistency between heroes

This is code reusability — OOP's biggest selling point. The more objects you need, the more this pays off.

Reading and updating properties directly

Even after an instance is created, you can read and change its public properties directly — just like you would with a plain object:

const aragorn = new Character("Aragorn", 200, "warrior");

// Read a property
console.log(aragorn.name);  // "Aragorn"
console.log(aragorn.hp);    // 200

// Update a property
aragorn.hp = 150;
console.log(aragorn.hp);    // 150 — updated

// Add a new property (public properties can be added after creation)
aragorn.weapon = "Excalibur";
console.log(aragorn.weapon); // "Excalibur"

Checking what something is — instanceof and typeof

Two tools beginners often need but aren't told about:

const aragorn = new Character("Aragorn", 200, "warrior");

// instanceof — is this object an instance of a particular class?
console.log(aragorn instanceof Character); // true
console.log(aragorn instanceof Array);     // false

// typeof — what type of value is this?
console.log(typeof aragorn);              // "object" — all class instances are objects
console.log(typeof aragorn.name);         // "string"
console.log(typeof aragorn.hp);           // "number"
console.log(typeof aragorn.introduce);    // "function" — methods are functions

instanceof is useful when you have mixed data and need to confirm what you're working with before calling methods on it.

Storing instances in an array

In real apps you'll rarely make just three heroes. You'll make a list:

// Create a party of heroes
const party = [
  new Character("Aragorn", 200, "warrior"),
  new Character("Lyra",    140, "mage"),
  new Character("Kira",    160, "rogue"),
];

// Loop and call methods on each
party.forEach(hero => {
  console.log(hero.introduce());
});
// "I am Aragorn, a warrior with 200 HP."
// "I am Lyra, a mage with 140 HP."
// "I am Kira, a rogue with 160 HP."

// Find a hero by name
const mage = party.find(h => h.role === "mage");
console.log(mage.name); // "Lyra"

// Heal every hero
party.forEach(hero => hero.heal(30));

This is OOP + array methods working together — one of the most common patterns in JavaScript.


🔒 The Vault: Encapsulation

Your hero has HP. What if something outside your class reaches in and sets it to a billion? Or -999?

const hero = new Character("Aragorn", 200);
hero.hp = 9999999; // anyone can just... do this

That's dangerous. Real characters should have controlled access — you can read HP through a method, but you can't smash it directly from outside.

Encapsulation means keeping an object's internal data protected and only exposing what needs to be exposed.

Scene 04 — Encapsulation force shield

JavaScript uses # to mark fields as private — they're completely invisible and unreachable from outside the class:

class Character {
  #hp;  // private field — nothing outside can touch this

  constructor(name, hp) {
    this.name = name;
    this.#hp  = hp;   // set it inside the class — OK
  }

  // Controlled access — the only way to read hp
  getHp() {
    return this.#hp;
  }

  // Controlled mutation — the only way to change hp
  takeDamage(amount) {
    if (amount > 0) {
      this.#hp -= amount;
    }
  }

  heal(amount) {
    this.#hp = Math.min(this.#hp + amount, 200); // can't exceed 200
  }
}

const hero = new Character("Aragorn", 200);

// Correct — use the methods
hero.takeDamage(50);
console.log(hero.getHp());  // 150

// Blocked — private field can't be accessed
hero.#hp = 9999;            // SyntaxError
console.log(hero.#hp);      // SyntaxError

💡 Encapsulation = trust boundaries. You give the outside world just enough access to do what they need — no more. The # field is the force shield. Methods are the controlled gates.

Public vs Private — the full picture

Here's how to decide what should be public and what should be private:

Public (this.name) Private (#hp)
Accessible from outside? Yes No
Can be read directly? hero.name hero.#hp → error
Can be changed directly? hero.name = "x" hero.#hp = 0 → error
When to use Labels, IDs, display names Sensitive state that needs validation
class Character {
  #hp;           // private — needs validation before changing
  #maxHp;        // private — should never change after creation

  constructor(name, hp) {
    this.name = name;   // public  — fine to read/change freely
    this.role = "hero"; // public  — no danger in changing this
    this.#hp    = hp;
    this.#maxHp = hp;   // store the max so heal() can't exceed it
  }

  // Controlled ways to interact with #hp
  takeDamage(amount) {
    if (typeof amount !== "number" || amount < 0) return; // guard clause
    this.#hp = Math.max(0, this.#hp - amount);
  }

  heal(amount) {
    this.#hp = Math.min(this.#maxHp, this.#hp + amount); // can't exceed max
  }

  getHp()    { return this.#hp; }
  getMaxHp() { return this.#maxHp; }

  getHpBar() {
    const pct  = Math.round((this.#hp / this.#maxHp) * 10);
    const bar  = "█".repeat(pct) + "░".repeat(10 - pct);
    return `\({this.name}: [\){bar}] \({this.#hp}/\){this.#maxHp}`;
  }
}

const hero = new Character("Aragorn", 200);
hero.takeDamage(80);
console.log(hero.getHpBar()); // "Aragorn: [██████░░░░] 120/200"
hero.heal(50);
console.log(hero.getHpBar()); // "Aragorn: [████████░░] 170/200"
hero.takeDamage(999);
console.log(hero.getHp());    // 0 — floor is 0, not negative

Using get and set — JavaScript getters and setters

JavaScript has a cleaner syntax for controlled property access called getters and setters. They let you access a method like a property (no parentheses):

class Character {
  #hp;

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

  // getter — accessed as hero.hp, not hero.hp()
  get hp() {
    return this.#hp;
  }

  // setter — triggered when you write hero.hp = value
  set hp(value) {
    if (value < 0) {
      this.#hp = 0;
    } else if (value > 200) {
      this.#hp = 200;
    } else {
      this.#hp = value;
    }
  }
}

const hero = new Character("Aragorn", 200);

// Reads like a property — but runs the getter function
console.log(hero.hp); // 200

// Writes like a property — but runs the setter function (with validation!)
hero.hp = 150;
console.log(hero.hp); // 150

hero.hp = -50;        // triggers setter
console.log(hero.hp); // 0 — negative clamped to 0

hero.hp = 9999;       // triggers setter
console.log(hero.hp); // 200 — max clamped to 200

💡 Getters and setters look like properties but run code. They're ideal when you want simple hero.hp syntax on the outside but validation logic on the inside. It's the best of both worlds.


🚨 Common Beginner Mistakes — and How to Avoid Them

These are the exact errors every beginner hits. Read them now, avoid them later.

Mistake 1 — Commas between methods (the JSON reflex)

Beginners who just learned objects instinctively add commas between methods. Don't.

// WRONG — no commas between class methods
class Character {
  constructor(name) {
    this.name = name;
  },                      // ← SyntaxError! No comma here
  attack() {
    console.log("strike");
  },                      // ← SyntaxError!
  heal() {
    console.log("heal");
  }
}

// CORRECT — no commas, just methods one after another
class Character {
  constructor(name) {
    this.name = name;
  }
  attack() {
    console.log("strike");
  }
  heal() {
    console.log("heal");
  }
}

Mistake 2 — Forgetting this inside methods

Inside a method, you must use this to access the instance's own properties. Bare variable names won't work.

class Character {
  constructor(name, hp) {
    this.name = name;
    this.hp   = hp;
  }

  // WRONG — "name" and "hp" are not in scope here
  status() {
    return `\({name} has \){hp} HP`; // ReferenceError: name is not defined
  }

  // CORRECT — always use this.name and this.hp
  status() {
    return `\({this.name} has \){this.hp} HP`;
  }
}

Mistake 3 — Using function keyword inside a class

Methods inside a class don't use the function keyword. That syntax is for standalone functions.

// WRONG — "function" keyword inside class body
class Character {
  constructor(name) { this.name = name; }
  function attack() {   // ← SyntaxError
    console.log("hit");
  }
}

// CORRECT — just the method name and ()
class Character {
  constructor(name) { this.name = name; }
  attack() {
    console.log("hit");
  }
}

Mistake 4 — Calling a method without parentheses

A method without () gives you the function itself, not the result of calling it.

const hero = new Character("Aragorn", 200);

console.log(hero.status);    // [Function: status] — you got the function, not its result
console.log(hero.status());  // "Aragorn: 200 HP" — called correctly

Mistake 5 — Expecting methods to share data between instances

Each instance has its own copy of properties. Methods run in the context of the instance they're called on.

const aragorn = new Character("Aragorn", 200);
const lyra    = new Character("Lyra", 140);

aragorn.hp = 50;

// This only changed aragorn's hp — not lyra's
console.log(aragorn.hp); // 50
console.log(lyra.hp);    // 140 ← completely separate

⚔️ Mini-Game — Build It As You Read

Let's build a tiny two-hero battle sim step by step. Run each block in your browser console to see it come alive.

Step 1 — Forge the hero class:

class Hero {
  #hp;
  #maxHp;

  constructor(name, hp, power) {
    this.name   = name;
    this.power  = power;
    this.#hp    = hp;
    this.#maxHp = hp;
    this.alive  = true;
  }

  get hp() { return this.#hp; }

  takeDamage(amount) {
    this.#hp = Math.max(0, this.#hp - amount);
    if (this.#hp === 0) {
      this.alive = false;
    }
  }

  heal(amount) {
    this.#hp = Math.min(this.#maxHp, this.#hp + amount);
  }

  getHpBar() {
    const filled = Math.round((this.#hp / this.#maxHp) * 10);
    return "█".repeat(filled) + "░".repeat(10 - filled);
  }

  status() {
    return `\({this.name.padEnd(10)} [\){this.getHpBar()}] \({this.#hp}/\){this.#maxHp}`;
  }
}

Step 2 — Add an attack method:

class Hero {
  #hp;
  #maxHp;

  constructor(name, hp, power) {
    this.name   = name;
    this.power  = power;
    this.#hp    = hp;
    this.#maxHp = hp;
    this.alive  = true;
  }

  get hp() { return this.#hp; }

  attack(target) {
    if (!this.alive) {
      console.log(`${this.name} is fallen and cannot attack.`);
      return;
    }
    const dmg = Math.floor(Math.random() * this.power) + 1;
    target.takeDamage(dmg);
    console.log(`\({this.name} hits \){target.name} for \({dmg} dmg!  \){target.status()}`);
  }

  takeDamage(amount) {
    this.#hp = Math.max(0, this.#hp - amount);
    if (this.#hp === 0) this.alive = false;
  }

  heal(amount) {
    this.#hp = Math.min(this.#maxHp, this.#hp + amount);
  }

  getHpBar() {
    const filled = Math.round((this.#hp / this.#maxHp) * 10);
    return "█".repeat(filled) + "░".repeat(10 - filled);
  }

  status() {
    const filled = Math.round((this.#hp / this.#maxHp) * 10);
    const bar    = "█".repeat(filled) + "░".repeat(10 - filled);
    return `\({this.name.padEnd(10)} [\){bar}] \({this.#hp}/\){this.#maxHp}`;
  }
}

Step 3 — Summon heroes and run the battle:

const aragorn = new Hero("Aragorn", 100, 25);
const goblin  = new Hero("Goblin",   60, 15);

console.log("⚔️  BATTLE START");
console.log(aragorn.status());
console.log(goblin.status());
console.log("─".repeat(40));

let round = 1;
while (aragorn.alive && goblin.alive) {
  console.log(`\nRound ${round}`);
  aragorn.attack(goblin);
  if (goblin.alive) goblin.attack(aragorn);
  round++;
}

console.log("\n🏆 BATTLE OVER");
if (aragorn.alive) console.log("Aragorn wins!");
else               console.log("Goblin wins!");

Expected output (randomized damage, so yours will differ):

⚔️  BATTLE START
Aragorn    [██████████] 100/100
Goblin     [██████████] 60/60
────────────────────────────────────────

Round 1
Aragorn hits Goblin for 18 dmg!   Goblin     [███████░░░] 42/60
Goblin hits Aragorn for 11 dmg!   Aragorn    [█████████░] 89/100

Round 2
Aragorn hits Goblin for 22 dmg!   Goblin     [████░░░░░░] 20/60
Goblin hits Aragorn for 9 dmg!    Aragorn    [████████░░] 80/100
...

🏆 BATTLE OVER
Aragorn wins!

You just built a working battle game using a single class. Every concept in this blog — constructor, this, methods, private fields, encapsulation — is in that code.

Here's the complete Character class with everything covered — annotated so every line connects back to a concept:

The complete class — annotated spellbook
class Character {
  #hp;                                   // ← private field (encapsulation)

  constructor(name, hp, role) {          // ← constructor (runs on new)
    this.name  = name;                   // ← this = the instance
    this.role  = role;
    this.#hp   = hp;
    this.alive = true;
  }

  attack(target) {                       // ← method (shared by all instances)
    if (!this.alive) return;
    target.takeDamage(50);
    console.log(`\({this.name} attacks \){target.name}!`);
  }

  takeDamage(amount) {
    this.#hp -= amount;
    if (this.#hp <= 0) {
      this.#hp   = 0;
      this.alive = false;
      console.log(`${this.name} has fallen.`);
    }
  }

  heal(amount) {
    this.#hp = Math.min(this.#hp + amount, 200);
  }

  getHp() { return this.#hp; }          // ← getter (controlled access)

  status() {
    return `\({this.name} [\){this.role}] HP: ${this.getHp()}`;
  }
}

// ── Summoning heroes ──
const aragorn = new Character("Aragorn", 200, "warrior");
const goblin  = new Character("Goblin",   80, "enemy");

console.log(aragorn.status()); // "Aragorn [warrior] HP: 200"
aragorn.attack(goblin);        // "Aragorn attacks Goblin!"
console.log(goblin.status());  // "Goblin [enemy] HP: 30"
aragorn.attack(goblin);        // "Goblin has fallen."

🔄 Class vs Plain Object — When to Use What

Now that you know both, here's when to reach for each:

Situation Use Why
One-off config ({ debug: true }) Plain object Simpler, no overhead
Describing one thing once ({ name, age }) Plain object No need for a blueprint
Creating many similar items Class Blueprint pays off
Items need shared methods Class Define once, use everywhere
Data needs validation / protection Class Private fields + setters
You need instanceof checks Class Works with instanceof
// Plain object — fine for a one-off config
const settings = { theme: "dark", language: "en", fontSize: 16 };

// Class — better when you make many, or need methods + validation
class UserProfile {
  constructor(username, email) {
    this.username = username;
    this.email    = email;
    this.joinedAt = new Date();
  }
  getDisplayName() {
    return `@${this.username}`;
  }
}
const user1 = new UserProfile("aryan_dev", "aryan@example.com");
const user2 = new UserProfile("priya_codes", "priya@example.com");
class           →  the blueprint / template
constructor()   →  runs on "new" — wires up this
this            →  refers to the current instance
new ClassName() →  creates a new instance from the class
methods         →  functions inside the class — every instance gets them
field           →  private — can't be accessed from outside
encapsulation   →  hiding internal state, exposing only what's needed
Concept What it means Code
Class Blueprint for objects class Character {}
Instance One object made from the class new Character("A", 200)
Constructor Runs once on creation constructor(name) { this.name = name }
this The current instance this.hp = hp
Method Function inside the class attack() { ... }
Private field Hidden internal data #hp
Encapsulation Controlled access to data getHp() { return this.#hp }

🏋️ Today's Assignment

Open your browser console. Forge your first class.

// Step 1 — Create a Student class
class Student {
  constructor(name, age, course) {
    this.name   = name;
    this.age    = age;
    this.course = course;
  }

  introduce() {
    return `Hi! I'm \({this.name}, \){this.age} years old, studying ${this.course}.`;
  }

  study(topic) {
    console.log(`\({this.name} is studying \){topic}. 📚`);
  }
}

// Step 2 — Create multiple students
const aryan = new Student("Aryan", 22, "JavaScript");
const priya = new Student("Priya", 20, "Python");
const dev   = new Student("Dev",   23, "React");

// Step 3 — Call methods on each
console.log(aryan.introduce()); // "Hi! I'm Aryan, 22 years old..."
console.log(priya.introduce());
aryan.study("OOP");

// Step 4 — Verify independence
aryan.age = 23;
console.log(priya.age); // 20 — unaffected

// Step 5 — Bonus: add a private grade field
class StudentV2 {
  #grade = 0;

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

  setGrade(g) {
    if (g >= 0 && g <= 100) this.#grade = g;
  }

  getGrade() {
    return this.#grade;
  }
}

const student = new StudentV2("Aryan");
student.setGrade(92);
console.log(student.getGrade()); // 92
console.log(student.#grade);     // SyntaxError — private!

🔑 Key Takeaways

  • A class is a blueprint — not an object itself, but the template for creating objects

  • The constructor runs automatically on new — it wires up this with the object's initial data

  • this refers to the specific instance being created or used — each instance has its own this

  • Methods are functions inside the class — defined once, available on every instance

  • new ClassName() creates a fresh, independent instance

  • Private fields (#) enforce encapsulation — they can't be read or changed from outside

  • Encapsulation protects data integrity — expose only what needs to be exposed

  • Getters/setters (get / set) let you control reads and writes like properties but with logic inside

  • instanceof checks which class an object was created from

  • Instances can be stored in arrays and combined with array methods (forEach, find, filter)

  • Commas between methods → SyntaxError. function keyword inside class → SyntaxError. These are the two most common class mistakes.


🎯 What's Next?

Episode 04: this, call, apply, bind — The Actor with Amnesia

Every time the actor walks onto a new stage, he forgets who he is. His agent carries three tools — call, apply, and bind — to remind him. Each one works differently. All three solve the same problem: this has gone missing.

We'll cover why this changes depending on context, and exactly how to control it.


Enjoyed this episode? Drop a reaction and share with a friend who's learning JavaScript. See you at Episode 04. ⚔️