JavaScriptIntermediate

Objects, Prototypes & Classes: JavaScript's Object Model

JavaScript doesn't have classical inheritance — it has prototype chains, with class syntax as sugar on top. Understand `this`, prototypes, and classes to read any codebase with confidence.

4 sections · ~30 min · 5-question quiz (pass ≥ 70%)

1The Prototype Chain

Every JavaScript object has a hidden link to another object: its prototype. When you read a property that the object doesn't have, the engine walks up this prototype chain until it finds it — or hits null.

const animal = { eats: true };
const rabbit = Object.create(animal); // rabbit's prototype is animal
rabbit.hops = true;

rabbit.hops;  // true  — own property
rabbit.eats;  // true  — found on the prototype
rabbit.flies; // undefined — chain exhausted (animal → Object.prototype → null)

This is why every array "has" .map — the method lives once on Array.prototype, and all arrays delegate to it. Delegation, not copying: a billion arrays, one map function.

Inspect the chain with Object.getPrototypeOf(obj), and check ownership with obj.hasOwnProperty("x").

2`this`: Four Binding Rules

this is determined by how a function is called, not where it's defined. Four rules, in order of precedence:

// 1. new binding — this = the freshly created object
const u = new User("Ada");

// 2. Explicit binding — this = whatever you pass
greet.call(user);  greet.apply(user);  const bound = greet.bind(user);

// 3. Method call — this = the object before the dot
user.greet();      // this === user

// 4. Plain call — this = undefined (strict mode) or globalThis
greet();           // usually a bug when this is involved

The classic pitfall — detaching a method loses its receiver:

const greet = user.greet;
greet(); // this is undefined — the "object before the dot" is gone

Arrow functions have no this of their own — they inherit it lexically from the enclosing scope. That's why arrows are perfect for callbacks inside methods, and wrong as methods themselves.

3Classes: Sugar Over Prototypes

ES2015 class syntax is a cleaner way to set up the same prototype machinery:

class Animal {
  constructor(name) {
    this.name = name;          // own property on each instance
  }
  speak() {                    // lives ONCE on Animal.prototype
    return `${this.name} makes a sound`;
  }
  #secret = "hidden";          // true private field (# prefix)
  static create(name) {        // on the class itself, not instances
    return new Animal(name);
  }
}

const dog = new Animal("Rex");
dog.speak();                        // "Rex makes a sound"
Object.getPrototypeOf(dog) === Animal.prototype; // true — same old chain

Under the hood nothing new happened: speak sits on Animal.prototype, and instances delegate to it through the prototype chain. typeof Animal is even "function".

4Inheritance with extends — and When Not to Use It

extends chains prototypes for you:

class Dog extends Animal {
  constructor(name, breed) {
    super(name);               // MUST call super before touching this
    this.breed = breed;
  }
  speak() {
    return `${super.speak()} — woof!`;  // call the parent version
  }
}

const rex = new Dog("Rex", "Lab");
rex.speak();          // "Rex makes a sound — woof!"
rex instanceof Dog;   // true
rex instanceof Animal; // true — the chain: rex → Dog.prototype → Animal.prototype

Guidance for real codebases:

  • Keep hierarchies shallow — one level of extends is usually plenty.
  • Prefer composition ("has-a") over inheritance ("is-a") when behavior is mixed and matched.
  • Getters/setters (get fullName()) and static members round out the toolbox.

Ready to test yourself?

Sign in to take the quiz, track progress, and earn a certificate.

Sign in