Prototypal inheritance is one of JavaScript's most powerful yet misunderstood features. Unlike classical inheritance found in languages like Java or C++, JavaScript uses a prototype-based approach that's both flexible and elegant. Let's unpack this concept from the ground up.
The Problem: Code Reusability
Imagine you're building a user management system. You have a basic user object with common properties and methods. Now you need to create admin and guest variants. Should you copy-paste all the user code? Absolutely not! This is where prototypal inheritance shines—it lets you build new objects on top of existing ones.
The Hidden [[Prototype]] Property
Every JavaScript object has a secret weapon: a hidden property called [[Prototype]]. Think of it as a reference pointer that says, "If you can't find what you're looking for in me, check this other object."
This [[Prototype]] can point to:
- Another object (your prototype)
-
null(end of the chain)
When you try to access a property or method that doesn't exist on an object, JavaScript automatically searches up the prototype chain until it finds it—or reaches null.
Setting Up Prototypes with __proto__
The historical way to access [[Prototype]] is through __proto__. Here's a classic example:
let animal = {
eats: true,
walk() {
console.log("Animal walks");
}
};
let rabbit = {
jumps: true
};
// Set animal as rabbit's prototype
rabbit.__proto__ = animal;
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Animal walks" (inherited method)
When we access rabbit.eats, JavaScript thinks: "I don't see eats in rabbit... let me check its prototype. Ah! Found it in animal!"
Important: __proto__ vs [[Prototype]]
Here's a crucial distinction that trips up many developers:
-
[[Prototype]]is the actual internal property -
__proto__is a getter/setter for accessing it
Modern JavaScript recommends using Object.getPrototypeOf() and Object.setPrototypeOf() instead, but __proto__ remains widely supported and intuitively clear for learning.
Prototype Chains: Going Deeper
Prototypes can form chains of arbitrary length:
let animal = {
eats: true,
walk() {
console.log("Animal walks");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
longEar.walk(); // "Animal walks" (found in animal)
console.log(longEar.jumps); // true (found in rabbit)
console.log(longEar.eats); // true (found in animal)
JavaScript searches: longEar → rabbit → animal → Object.prototype → null
Prototype Chain Limitations
- No circular references: You can't create infinite loops
- Only objects or null: Primitives like strings or numbers are ignored
- Single inheritance: An object can't inherit from multiple prototypes directly
Writing vs Reading: A Critical Difference
Here's where things get interesting. Prototypes are read-only lookup chains. When you write to a property, it goes directly to the object itself:
let animal = {
eats: true,
walk() {
console.log("Generic animal walk");
}
};
let rabbit = {
__proto__: animal
};
// This creates a new method on rabbit, doesn't modify animal
rabbit.walk = function() {
console.log("Rabbit bounce!");
};
rabbit.walk(); // "Rabbit bounce!" (own method)
animal.walk(); // "Generic animal walk" (unchanged)
The Accessor Property Exception
Accessor properties (getters/setters) are the exception. When you assign to an accessor property, you're actually calling its setter function:
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
console.log(admin.fullName); // "John Smith" (getter from prototype)
admin.fullName = "Alice Cooper"; // Calls setter from prototype
console.log(admin.fullName); // "Alice Cooper"
The Magic of this: Always Points to the Caller
This is perhaps the most important concept: this is determined by the object before the dot, not where the method is defined.
let animal = {
walk() {
if (!this.isSleeping) {
console.log(`${this.name} walks`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
let cat = {
name: "Fluffy",
__proto__: animal
};
rabbit.sleep(); // Sets rabbit.isSleeping = true
cat.sleep(); // Sets cat.isSleeping = true
console.log(rabbit.isSleeping); // true
console.log(cat.isSleeping); // true
console.log(animal.isSleeping); // undefined
Even though sleep() is defined in animal, when called on rabbit or cat, this refers to the calling object. This means methods are shared, but state is not—a beautiful design pattern!
Looping and Enumeration: Own vs Inherited
The for...in loop iterates over both own and inherited properties:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Only own properties
console.log(Object.keys(rabbit)); // ["jumps"]
// Both own and inherited
for (let prop in rabbit) {
console.log(prop); // "jumps", then "eats"
}
To distinguish between own and inherited properties:
for (let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
console.log(`Own: ${prop}`);
} else {
console.log(`Inherited: ${prop}`);
}
}
// Own: jumps
// Inherited: eats
Why Doesn't hasOwnProperty Show Up in the Loop?
Excellent question! hasOwnProperty itself is inherited from Object.prototype, but it doesn't appear in for...in loops because it's marked as non-enumerable (enumerable: false). This is true for all Object.prototype methods.
Key Takeaways
- Prototypal inheritance enables code reuse without copying
-
[[Prototype]]is the internal link,__proto__is how we access it - Reading follows the chain, writing happens directly on the object
-
thisalways refers to the calling object, not where the method is defined - Methods are shared, state is not—perfect for efficient object creation
-
Most methods like
Object.keys()ignore inherited properties, onlyfor...inincludes them
Practical Applications
Understanding prototypes is crucial for:
- Working with JavaScript classes (which use prototypes under the hood)
- Understanding how built-in objects like Arrays and Dates work
- Optimizing memory usage by sharing methods across instances
- Debugging unexpected behavior in object hierarchies
- Leveraging advanced patterns like mixins and delegation
Prototypal inheritance might seem strange at first, especially if you come from a classical OOP background. But once it clicks, you'll appreciate its simplicity and power. It's the foundation of JavaScript's object model and understanding it deeply will make you a much more effective JavaScript developer.
Top comments (0)