The Mysterious World of JavaScript Prototypes: A Detective Story

The Mysterious World of JavaScript Prototypes: A Detective Story

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 :

  1. 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.
      
  2. 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

  1. 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
      
  2. 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?

  1. Animal is a base constructor function with a method on its prototype.

  2. Dog is a derived constructor that calls Animal to initialize its properties.

  3. We set Dog.prototype to an object created from Animal.prototype, establishing the inheritance.

  4. We manually reset Dog.prototype.constructor to Dog to maintain consistency.

  5. Instances of Dog inherit methods from both Animal and Dog.

  6. Prototype chaining allows methods from Animal to be accessed by Dog 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!" 🕵️‍♂️