Prototypes & Inheritance
JavaScript's inheritance model is prototype-based, not class-based. Understanding prototypes reveals how classes, Object.create, and method sharing actually work.
The Prototype Chain
Every JavaScript object has an internal link to another object called its prototype. When you try to access a property on an object, JavaScript first looks at the object itself, then walks up the prototype chain until it finds the property or reaches null.
const animal = {
breathe() {
return `${this.name} breathes`;
},
};
const dog = {
name: "Rex",
bark() {
return "Woof!";
},
};
// Set dog's prototype to animal
Object.setPrototypeOf(dog, animal);
console.log(dog.bark()); // "Woof!" โ own method
console.log(dog.breathe()); // "Rex breathes" โ inherited from animal
Object.create()
The cleanest way to create prototypal inheritance:
const vehicleProto = {
start() { return `${this.model} started`; },
stop() { return `${this.model} stopped`; },
};
const car = Object.create(vehicleProto);
car.model = "Toyota Camry";
car.wheels = 4;
console.log(car.start()); // "Toyota Camry started"
console.log(car.stop()); // "Toyota Camry stopped"
console.log(Object.getPrototypeOf(car) === vehicleProto); // true
Constructor Functions (Pre-ES6)
Before classes, constructors were the standard way to create objects with shared methods:
function Person(name, age) {
this.name = name;
this.age = age;
}
// Methods go on the prototype (shared, not copied per instance)
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
Person.prototype.birthday = function() {
this.age++;
};
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);
console.log(alice.greet()); // "Hi, I'm Alice"
alice.birthday();
console.log(alice.age); // 31
console.log(bob.age); // 25 โ unaffected
// Both share the same greet function (memory efficient)
console.log(alice.greet === bob.greet); // true
When you call new Person():
- A new empty object is created
- Its prototype is set to
Person.prototype thisinside the function refers to the new object- The object is returned automatically
ES6 Classes
Classes are syntactic sugar over prototype-based inheritance. Under the hood, they work exactly the same way:
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} says ${this.sound}`;
}
toString() {
return `Animal(${this.name})`;
}
}
class Dog extends Animal {
constructor(name) {
super(name, "Woof"); // call parent constructor
this.tricks = [];
}
learn(trick) {
this.tricks.push(trick);
return this; // enable chaining
}
perform() {
return `${this.name} can: ${this.tricks.join(", ")}`;
}
}
const rex = new Dog("Rex");
rex.learn("sit").learn("stay").learn("roll over");
console.log(rex.speak()); // "Rex says Woof"
console.log(rex.perform()); // "Rex can: sit, stay, roll over"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
Class Features
class BankAccount {
// Public field
currency = "USD";
// Private field (ES2022)
#balance;
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance;
}
// Getter (access like a property)
get balance() {
return this.#balance;
}
// Static method (on the class, not instances)
static validate(amount) {
return typeof amount === "number" && amount > 0;
}
deposit(amount) {
if (!BankAccount.validate(amount)) throw new Error("Invalid amount");
this.#balance += amount;
return this;
}
toString() {
return `${this.owner}'s account: ${this.#balance} ${this.currency}`;
}
}
const account = new BankAccount("Alice", 1000);
account.deposit(500);
console.log(account.balance); // 1500
console.log(String(account)); // "Alice's account: 1500 USD"
// account.#balance; // SyntaxError โ private!
Checking Prototype Relationships
const arr = [1, 2, 3];
// instanceof
arr instanceof Array // true
arr instanceof Object // true (all objects inherit from Object)
// isPrototypeOf
Array.prototype.isPrototypeOf(arr) // true
// getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype // true
// hasOwnProperty vs inherited
arr.hasOwnProperty("length") // true โ own property
arr.hasOwnProperty("push") // false โ inherited from Array.prototype
// In operator checks own + prototype chain
"push" in arr // true
The Prototype Chain for Arrays
Understanding that built-in methods are on prototypes helps you understand "monkey patching" (adding to prototypes) and why it's usually a bad idea:
// Array prototype chain:
// arr โ Array.prototype โ Object.prototype โ null
// Array.prototype has: push, pop, map, filter, etc.
// Object.prototype has: toString, hasOwnProperty, etc.
// โ Bad: modifying built-in prototypes
Array.prototype.sum = function() {
return this.reduce((a, b) => a + b, 0);
};
[1, 2, 3].sum(); // 6 โ works, but pollutes all arrays!
โ ๏ธDon't modify built-in prototypes
Adding to Array.prototype, Object.prototype, etc. affects every object of that type in your entire application and any libraries you use. It causes hard-to-debug conflicts. Use utility functions or subclass instead.
Key Takeaways
- Every object has a prototype โ property lookup walks up the chain
Object.create(proto)creates an object with a specific prototype- Constructor functions +
newis the pre-ES6 way to set up prototypes - ES6 classes are syntactic sugar โ they're still prototype-based under the hood
instanceofchecks the prototype chain- Private class fields (
#field) are genuinely private in modern JS
Ready to test your knowledge?
Take a quiz on what you just learned.