Introduction
Welcome to the world of JavaScript, where objects inherit from other objects, and the term "prototype" is often thrown around like a detective's badge. If you've ever felt like you're investigating a mystery when dealing with prototype
, __proto__
, and inheritance, don’t worry—you’re not alone. Today, we’re uncovering the secrets behind JavaScript’s prototype system with a thrilling detective story. Grab your magnifying glass, and let’s begin.
Chapter 1: What is a Prototype? 🕵️♂️
Meet Detective Java, a seasoned investigator in the coding underworld. One day, a junior detective (that’s you!) asks, "What exactly is a prototype?"
Detective Java smirks. "In JavaScript, every object has a hidden blueprint, known as its prototype. It holds shared properties and methods that other objects can use. It’s like a secret manual that every detective follows."
Example:
function Detective(name) {
this.name = name;
}
Detective.prototype.solveCase = function() {
console.log(`${this.name} is solving a mystery! 🕵️♂️`);
};
const sherlock = new Detective("Sherlock Holmes");
sherlock.solveCase(); // Sherlock Holmes is solving a mystery! 🕵️♂️
Sherlock doesn’t have solveCase()
it inside him directly, but because of the prototype, he still has access to it. That’s the power of prototypes!
Chapter 2: Prototype vs. Inheritance 🔍
"Alright," you say, "so prototypes are like a shared manual. But isn't that just inheritance?"
Detective Java leans in. "Not quite. In classical inheritance, objects inherit from classes in a strict hierarchy. JavaScript uses prototypal inheritance, where objects inherit directly from other objects. It’s like learning skills from experienced detectives instead of following rigid academy rules."
Classical Inheritance Example
class Detective {
constructor(name) {
this.name = name;
}
solveCase() {
console.log(`${this.name} is solving a case!`);
}
}
class SuperDetective extends Detective {
hackSystem() {
console.log(`${this.name} is hacking the system! 💻`);
}
}
const batman = new SuperDetective("Batman");
batman.solveCase(); // Batman is solving a case!
batman.hackSystem(); // Batman is hacking the system!
Prototypal Inheritance Example
const detective = {
solveCase() {
console.log("Solving a case... 🔍");
}
};
const agent = Object.create(detective);
agent.solveCase(); // Solving a case... 🔍
Chapter 3: [[Prototype]]
vs __proto__
🧐
Detective Java warns, "This is where many detectives get lost. [[Prototype]]
is JavaScript’s internal mechanism that links objects, while __proto__
is an older, visible way to access it."
Prototype
Definition:
prototype
is a property of a constructor function. It is used to add properties and methods to the constructor function, which will be shared by all instances created by that constructor.Usage: Use
prototype
when you want to define methods and properties that should be shared among all instances of a constructor function.Example :
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.describe = function () {
return `This is a ${this.type}.`;
};
const car = new Vehicle("car");
console.log(car.describe()); // This is a car.
__proto__
Definition:
__proto__
is an internal property of an object that points to the prototype of the constructor function that created the object. It is used to access or set the prototype of an individual object.Usage: Use
__proto__
when you want to set or access the prototype of an individual object, especially when you need to change the prototype chain dynamically.
Example:
const bike = {
type: "bike"
};
bike.__proto__ = Vehicle.prototype;
console.log(bike.describe()); // This is a bike.
Combined Example:
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.describe = function () {
return `This is a ${this.type}.`;
};
const car = new Vehicle("car");
console.log(car.describe()); // This is a car.
const bike = { type: "bike" };
bike.__proto__ = Vehicle.prototype;
console.log(bike.describe()); // This is a bike.
Vehicle.prototype.start = function () {
return `${this.type} is starting.`;
};
bike.__proto__.makeSound = function () {
return `${this.type} is making sound.`;
};
console.log(car.start()); // car is starting.
console.log(bike.start()); // bike is starting.
console.log(bike.makeSound()); // bike is making sound.
console.log(car.makeSound()); // car is making sound.
Avoid using __proto__
, and prefer Object.getPrototypeOf(obj)
.
Which one to use?
For defining shared methods and properties: Use
prototype
on the constructor function. This ensures that all instances created by the constructor function share the same methods and properties.For setting or accessing the prototype of an individual object: Use
__proto__
. This is useful when you need to change the prototype chain of an object dynamically.
Summary
Use
prototype
to define methods and properties that should be shared among all instances of a constructor function.Use
__proto__
to set or access the prototype of an individual object.
Chapter 4: When to Use prototype
and When Not to? 🤔
When to use :
Shared Methods and Properties:
Use
prototype
when you want to define methods and properties that should be shared among all instances of a constructor function.This is memory efficient because the methods and properties are not duplicated for each instance.
function Vehicle(type) { this.type = type; } Vehicle.prototype.describe = function () { return `This is a ${this.type}.`; }; const car = new Vehicle("car"); const bike = new Vehicle("bike"); console.log(car.describe()); // This is a car. console.log(bike.describe()); // This is a bike.
Inheritance:
Use
prototype
to set up inheritance between constructor functions.This allows instances of a derived constructor to inherit methods and properties from the base constructor.
function Animal(name) { this.name = name; } Animal.prototype.speak = function () { return `${this.name} makes a sound.`; }; function Dog(name) { Animal.call(this, name); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.speak = function () { return `${this.name} barks.`; }; const dog = new Dog("Rex"); console.log(dog.speak()); // Rex barks.
When Not to Use prototype
Instance-Specific Properties:
Do not use
prototype
for properties that are specific to each instance.These properties should be defined directly within the constructor function.
function Vehicle(type, color) { this.type = type; this.color = color; // Instance-specific property } const car = new Vehicle("car", "red"); const bike = new Vehicle("bike", "blue"); console.log(car.color); // red console.log(bike.color); // blue
Private Methods and Properties:
Do not use
prototype
for methods and properties that should be private.Private methods and properties should be defined within the constructor function using closures.
function Counter() { let count = 0; // Private property this.increment = function () { count++; return count; }; this.decrement = function () { count--; return count; }; } const counter = new Counter(); console.log(counter.increment()); // 1 console.log(counter.decrement()); // 0
Summary
Use
prototype
:For the shared methods and properties among all instances.
For setting up inheritance between constructor functions.
Do not use
prototype
:For instance-specific properties.
For private methods and properties.
Chapter 5: Why Avoid __proto__
? ⚠️
Manipulating __proto__
directly can make code slow and unpredictable. It breaks JavaScript’s optimization and can cause unintended side effects.
Bad Practice:
detective.__proto__ = anotherObject; // Don't do this!
Good Practice:
Object.setPrototypeOf(detective, anotherObject); // Safe alternative
Chapter 6: Prototypal Inheritance & Chaining 🧩
"So how does inheritance work?" you ask.
Detective Java leans forward. "Let’s break it down with an example."
Classical Prototypal Inheritance
function Animal(name) {
this.name = name;
}
// Adding a method to the prototype of Animal
Animal.prototype.speak = function () {
return `${this.name} makes a sound.`;
};
// Derived constructor function
function Dog(name, breed) {
Animal.call(this, name); // Call the parent constructor
this.breed = breed;
}
// Setting up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Adding a method to the prototype of Dog
Dog.prototype.bark = function () {
return `${this.name} barks.`;
};
// Creating an instance of Dog
const myDog = new Dog("Rex", "German Shepherd");
// Using inherited methods
console.log(myDog.speak()); // Rex makes a sound.
console.log(myDog.bark()); // Rex barks.
// Checking the prototype chain
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true
What’s Happening Here?
Animal
is a base constructor function with a method on its prototype.Dog
is a derived constructor that callsAnimal
to initialize its properties.We set
Dog.prototype
to an object created fromAnimal.prototype
, establishing the inheritance.We manually reset
Dog.prototype.constructor
toDog
to maintain consistency.Instances of
Dog
inherit methods from bothAnimal
andDog
.Prototype chaining allows methods from
Animal
to be accessed byDog
instances.
This is called prototype chaining—objects dynamically inherit from other objects, forming a chain of prototypes.
Checking the Prototype Chain
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true
This prototype chain ensures that myDog
one can access properties from Dog
, Animal
, and Object
.
Conclusion: Case Closed 🏁
"Prototypes in JavaScript are like detectives borrowing skills from their mentors instead of rigid class inheritance," you summarize.
"Exactly," says Detective Java, tipping his hat. "Use prototype
wisely, avoid __proto__
, and always keep your detective instincts sharp!" 🕵️♂️