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