Going DeeperPro· 55 min read

Object-Oriented JavaScript: Prototypes, Classes & Inheritance

How to model real things — users, products, animals — as reusable blueprints. Classes, the prototype chain behind them, and inheritance: a full mandatory section in every paid course.

What you will learn

  • Create objects from a class blueprint
  • Explain the prototype chain in plain words
  • Reuse code with extends (inheritance) and encapsulation

A class is a blueprint for objects

So far you have written objects one at a time with { }. But what if you need hundreds of users, each with the same shape and the same methods? A class is a blueprint (think cookie cutter) that stamps out as many objects (cookies) as you like, each with its own data but sharing the same behaviour.

You define a class with the class keyword. The special constructor method sets up each new object’s starting data, and this refers to the specific object being built. You create an object from the class with the new keyword.

One class, many objects via new
<script>
  class Dog {
    constructor(name, breed) {
      this.name = name;          // each dog gets its own name
      this.breed = breed;
    }
    bark() {
      return this.name + " says Woof!";
    }
  }

  const d1 = new Dog("Bruno", "Labrador");
  const d2 = new Dog("Lucy", "Beagle");

  document.write(d1.bark() + "<br>");
  document.write(d2.bark());
</script>
Live preview

Step by step:

  1. class Dog defines the blueprint: what data each dog holds and what it can do.
  2. new Dog("Bruno", "Labrador") stamps out a new dog object; the constructor runs, setting this.name and this.breed for that dog.
  3. We build a second dog, d2, with its own separate name and breed.
  4. Each object has the shared bark() method, but this.name inside it refers to the specific dog, so d1.bark() and d2.bark() differ.

Note: Output: Bruno says Woof! Lucy says Woof!

Watch out: Always use new to create an object from a class. Calling Dog("Bruno") without new throws a TypeError, because the constructor needs new to build a fresh object and set its this.

The prototype chain — where methods really live

Here is what is happening underneath. The bark method is not copied into every dog — that would waste memory. Instead, all dogs share one copy, stored on the class’s prototype. When you call d1.bark(), JavaScript first looks on the d1 object itself; not finding bark there, it follows a link up to Dog’s prototype and finds it. That hop-up-the-links search is the prototype chain.

Data lives on the object; methods live on the prototype
<script>
  class Dog {
    constructor(name) { this.name = name; }
    bark() { return "Woof"; }
  }
  const d = new Dog("Bruno");

  // bark is NOT on the object itself — it is found via the prototype
  document.write("Has own 'name'? " + d.hasOwnProperty("name") + "<br>");
  document.write("Has own 'bark'? " + d.hasOwnProperty("bark") + "<br>");
  document.write("Can it bark? " + d.bark());
</script>
Live preview

hasOwnProperty asks "is this directly on the object?". name is — it was set in the constructor for this specific dog. But bark is not on the object itself; it lives on Dog’s prototype, shared by all dogs. Yet d.bark() still works, because JavaScript follows the prototype chain up to find it. This is prototypal inheritance — the real engine under the class syntax.

Note: Output: Has own 'name'? true Has own 'bark'? false Can it bark? Woof

Tip: Plain-words summary: every object has a hidden link to a "parent" object (its prototype). Looking up a property checks the object first, then walks up these links until it is found or the chain ends. Classes are a clean, familiar syntax sitting on top of this mechanism.

Inheritance — build on an existing class

Inheritance lets one class build on another: a child class gets everything the parent has, and can add or override behaviour. Use extends to inherit, and super(...) inside the child constructor to run the parent’s setup first.

Cat extends Animal: inherit eat(), add meow()
<script>
  class Animal {
    constructor(name) { this.name = name; }
    eat() { return this.name + " is eating."; }
  }

  class Cat extends Animal {       // Cat inherits from Animal
    constructor(name, colour) {
      super(name);                 // run Animal's constructor first
      this.colour = colour;
    }
    meow() { return this.name + " says Meow!"; }
  }

  const c = new Cat("Misty", "grey");
  document.write(c.eat() + "<br>");    // inherited from Animal
  document.write(c.meow());            // Cat's own method
</script>
Live preview

Walk through the inheritance:

  1. Animal is the parent: it holds a name and an eat() method.
  2. class Cat extends Animal makes Cat a child that inherits everything from Animal.
  3. In Cat’s constructor, super(name) calls Animal’s constructor to set up name — you must do this before using this.
  4. Cat then adds its own data (colour) and its own method (meow).
  5. So c.eat() works (inherited from Animal) and c.meow() works (added by Cat) — code reused, then extended.

Note: Output: Misty is eating. Misty says Meow!

Watch out: In a subclass constructor you must call super() before using this. Forgetting it throws "Must call super constructor before accessing this" — a very common beginner error with inheritance.

Encapsulation — private fields

Encapsulation means hiding an object’s internal data so it can only be changed through controlled methods. Modern classes support truly private fields by prefixing the name with # — code outside the class cannot read or change them.

A #private field protects the balance
<script>
  class Account {
    #balance = 0;                 // private — only this class can touch it
    deposit(amount) { this.#balance += amount; return this.#balance; }
    getBalance()    { return this.#balance; }
  }

  const acc = new Account();
  acc.deposit(500);
  acc.deposit(200);
  document.write("Balance: " + acc.getBalance());
  // acc.#balance would be a SyntaxError — it is private
</script>
Live preview

#balance is a private field: the only way to change it is through the deposit method, and the only way to read it is getBalance. Outside code cannot reach acc.#balance directly — that protects your data from accidental or unwanted changes. This is encapsulation: a clean public interface around hidden internals.

Note: Output: Balance: 700

Tip: OOP in one breath: a class is a blueprint, new stamps out objects, methods live on the prototype (shared), extends/super give inheritance, and #fields give encapsulation. React class components, Node libraries and most interview questions build on exactly these ideas.

Q. In a subclass constructor that uses extends, what must you call before using this?

Answer: super() runs the parent class constructor and must be called before you use this in a subclass constructor — otherwise JavaScript throws an error.

✍️ Practice

  1. Write a Book class with a constructor and a describe() method, then create two books.
  2. Make a Student class that extends a Person class, calling super and adding one new method.
  3. Add a # private field to a class and prove it cannot be read from outside.

🏠 Homework

  1. Model a small system with inheritance — for example a Shape parent and Circle/Rectangle children, each with its own area() method.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →