What it is: JavaScript’s way of sharing properties and methods between objects without copying them.

The Core Concept - Hidden [[Prototype]] Property:

Every object has a secret hidden property called [[Prototype]] that points to another object (or null).

javascript

let animal = {
    eats: true,
    walk() {
        console.log("Animal walks");
    }
};
 
let rabbit = {
    jumps: true
};
 
// Connect rabbit to animal
rabbit.__proto__ = animal;

What happens: rabbit now has access to everything in animal without copying it.

How Property Lookup Works - The Chain:

When you access a property, JavaScript follows this process:

  1. Check the object itself - Does rabbit have the property?
  2. If not found, check prototype - Does rabbit.__proto__ (which is animal) have it?
  3. Keep going up the chain - Continue until found or reach null
console.log(rabbit.jumps); // true (found in rabbit itself)
console.log(rabbit.eats);  // true (found in animal via __proto__)
rabbit.walk();             // "Animal walks" (method from animal)

Visual flow:

rabbit.eats → not in rabbit → check rabbit.__proto__ (animal) → found eats: true

Longer Prototype Chains:

javascript

let animal = {
    eats: true,
    walk() { console.log("Walking..."); }
};
 
let rabbit = {
    jumps: true,
    __proto__: animal
};
 
let longEar = {
    earLength: 10,
    __proto__: rabbit
};
 
// Chain: longEar → rabbit → animal → Object.prototype → null
longEar.walk();  // "Walking..." (found in animal)
longEar.jumps;   // true (found in rabbit)
longEar.eats;    // true (found in animal)

Key Rules of Prototypes:

1. Reading goes up the chain, writing stays local:

javascript

let animal = {
    eats: true,
    walk() { console.log("Animal walks"); }
};
 
let rabbit = {
    __proto__: animal
};
 
// Reading - goes up chain
console.log(rabbit.eats); // true (from animal)
 
// Writing - stays in rabbit
rabbit.eats = false;
console.log(rabbit.eats); // false (rabbit's own property now)
console.log(animal.eats); // true (animal unchanged)

2. Methods use this of the calling object:

javascript

let animal = {
    name: "Generic Animal",
    introduce() {
        console.log(`I am ${this.name}`);
    }
};
 
let rabbit = {
    name: "Bunny",
    __proto__: animal
};
 
rabbit.introduce(); // "I am Bunny" (this = rabbit, not animal)

This is crucial: The method comes from animal, but this points to rabbit!

Rule : This always points to obj before dot , so this = rabit obj so used the name from there


Prototype Chain Limitations:

1. No circular references:

javascript

let a = {};
let b = {};
a.__proto__ = b;
b.__proto__ = a; // Error! Circular __proto__ value

2. __proto__ can only be object or null:

javascript

rabbit.__proto__ = "string"; // Ignored, __proto__ stays unchanged
rabbit.__proto__ = 42;       // Ignored
rabbit.__proto__ = null;     // OK - breaks the chain

3. Only one prototype per object:

javascript

// Can't inherit from multiple objects directly
rabbit.__proto__ = animal;
rabbit.__proto__ = machine; // This overwrites the previous one

Practical Example - Method Sharing:

javascript

let animal = {
    eat() {
        this.full = true;
        console.log(`${this.name} is eating`);
    },
    
    sleep() {
        this.sleeping = true;
        console.log(`${this.name} is sleeping`);
    }
};
 
let rabbit = {
    name: "White Rabbit",
    __proto__: animal
};
 
let snake = {
    name: "Python", 
    __proto__: animal
};
 
rabbit.eat();  // "White Rabbit is eating" (sets rabbit.full = true)
snake.eat();   // "Python is eating" (sets snake.full = true)
 
console.log(rabbit.full);  // true
console.log(snake.full);   // true
console.log(animal.full);  // undefined (animal wasn't modified)

__proto__ vs [[Prototype]]:

Important distinction:

  • [[Prototype]] = The actual hidden internal property
  • __proto__ = A getter/setter that accesses [[Prototype]]

javascript

// These are equivalent:
rabbit.__proto__ = animal;
Object.setPrototypeOf(rabbit, animal);
 
// These are equivalent:
console.log(rabbit.__proto__);
console.log(Object.getPrototypeOf(rabbit));

Modern way (preferred):

javascript

// Set prototype
Object.setPrototypeOf(rabbit, animal);
 
// Get prototype  
Object.getPrototypeOf(rabbit);
 
// Create object with specific prototype
let rabbit = Object.create(animal);

Mental Model:

Think of prototypes as a fallback mechanism:

  1. “Do I have this property/method myself?”
  2. “No? Let me check my prototype”
  3. “Not there either? Let me check my prototype’s prototype”
  4. Continue until found or hit null

Bottom line: Prototypal inheritance lets objects share functionality without copying. Objects can access properties/methods from their prototype chain, but modifications only affect the object itself. Use __proto__ for learning, but prefer Object.setPrototypeOf in real code


__proto__ is a getter/setter:

javascript

let animal = { eats: true };
let rabbit = {};
 
// SETTER - when you assign to __proto__
rabbit.__proto__ = animal;  // Sets [[Prototype]] = animal
 
// GETTER - when you read __proto__
console.log(rabbit.__proto__); // Reads [[Prototype]], returns animal

[[Prototype]] is the actual connection:

javascript

// [[Prototype]] is the hidden internal property that stores the reference
// Think of it like:
rabbit.[[Prototype]] = animal; // (This syntax doesn't exist, just for understanding)
 
// When you do rabbit.eats:
// 1. Check rabbit itself - not found
// 2. Check rabbit.[[Prototype]] (which is animal) - found!

Visual representation:

rabbit = {
    jumps: true,
    [[Prototype]]: → animal = { eats: true }
}

The inheritance chain lookup:

javascript

let grandparent = { walks: true };
let parent = { eats: true, __proto__: grandparent };
let child = { jumps: true, __proto__: parent };
 
child.walks; 
// 1. Check child - no 'walks'
// 2. Check child.[[Prototype]] (parent) - no 'walks' 
// 3. Check parent.[[Prototype]] (grandparent) - found 'walks'!

Modern alternatives do the same thing:

Old way (2 steps):

javascript

let rabbit = {};              // Create object
rabbit.__proto__ = animal;    // Set prototype

Modern way 1 (2 steps):

javascript

let rabbit = {};                        // Create object  
Object.setPrototypeOf(rabbit, animal);  // Set prototype

Modern way 2 (1 step):

javascript

let rabbit = Object.create(animal);  // Create object WITH prototype already set

All three create the same result:

javascript

rabbit.[[Prototype]] === animal  // true for all methods

Quick comparison:

javascript

let animal = { eats: true };
 
// Method 1: __proto__
let rabbit1 = { jumps: true };
rabbit1.__proto__ = animal;
 
// Method 2: setPrototypeOf  
let rabbit2 = { jumps: true };
Object.setPrototypeOf(rabbit2, animal);
 
// Method 3: Object.create + adding properties
let rabbit3 = Object.create(animal);
rabbit3.jumps = true;
 
// All are identical:
console.log(rabbit1.eats); // true
console.log(rabbit2.eats); // true  
console.log(rabbit3.eats); // true

The setter/getter behavior you mentioned:

javascript

let animal = { eats: true };
let rabbit = {};
 
// WRITE (setter) - sets the [[Prototype]]
rabbit.__proto__ = animal;
 
// READ (getter) - reads the [[Prototype]]
let parent = rabbit.__proto__; // Returns animal
 
// Check what happened:
console.log(Object.getPrototypeOf(rabbit) === animal); // true
console.log(rabbit.__proto__ === animal); // true

Your mental model is perfect:

  1. [[Prototype]] = The actual hidden “parent pointer”
  2. __proto__ = The way to get/set that pointer
  3. Inheritance chain = “If I don’t have it, check my parent, then grandparent, etc.”
  4. Object.create() = “Create object with parent already connected”