Full Stack • Java • System Design • Cloud • AI Engineering

JavaScript2026-06-06

JavaScript Prototype Chain: Complete Guide for Developers

Master JavaScript prototype chain, prototypal inheritance, __proto__, prototype property, Object.create(), property lookup mechanism, and memory optimization with detailed examples and diagrams.

JavaScript Prototype Chain: Complete Guide for Developers


📋 Table of Contents

  1. What is the Prototype Chain?
  2. Why Do We Need the Prototype Chain?
  3. Real-World Use Cases
  4. Syntax
  5. Internal Working
  6. Memory Diagram
  7. Data Flow Diagram
  8. Code Examples
  9. Common Mistakes
  10. Performance Considerations
  11. Interview Questions
  12. Cheat Sheet
  13. Summary

1. What is the Prototype Chain?

The Prototype Chain is JavaScript's mechanism for implementing inheritance. Unlike class-based languages (Java, C++), JavaScript uses prototypal inheritance where objects inherit directly from other objects.

Core Concept

┌─────────────────────────────────────────────────────────────┐
│         PROTOTYPE CHAIN FUNDAMENTALS                         │
└─────────────────────────────────────────────────────────────┘

Every JavaScript object has an internal link to another object
called its "prototype". That prototype object has a prototype
of its own, forming a chain that ends with null.

Object Instance
      ↓
   [[Prototype]] (internal link)
      ↓
Prototype Object
      ↓
   [[Prototype]]
      ↓
Object.prototype
      ↓
   [[Prototype]]
      ↓
    null

This chain is called the PROTOTYPE CHAIN

Key Terminology

┌─────────────────────────────────────────────────────────────┐
│         PROTOTYPE TERMINOLOGY                                │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  [[Prototype]]                                               │
│  ──────────────                                             │
│  • Internal property (not directly accessible)              │
│  • Links object to its prototype                            │
│  • Accessed via __proto__ (deprecated) or                   │
│    Object.getPrototypeOf()                                  │
│                                                              │
│  prototype Property                                          │
│  ──────────────────                                         │
│  • Property on constructor functions                        │
│  • Used when creating objects with 'new'                    │
│  • Contains methods shared by instances                     │
│                                                              │
│  __proto__ (Dunder Proto)                                    │
│  ────────────────────────                                   │
│  • Accessor property (getter/setter)                        │
│  • Provides access to [[Prototype]]                         │
│  • Deprecated but widely supported                          │
│  • Use Object.getPrototypeOf() instead                      │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2. Why Do We Need the Prototype Chain?

The Problem Without Prototypes

// ❌ Problem: Duplicate methods for each object
const user1 = {
    name: "John",
    greet: function() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

const user2 = {
    name: "Jane",
    greet: function() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

const user3 = {
    name: "Bob",
    greet: function() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

// Issues:
// • Each object has its own copy of greet()
// • Memory waste (3 identical functions)
// • Hard to maintain (change requires updating all)
// • No inheritance mechanism

The Solution With Prototypes

// ✅ Solution: Shared methods via prototype
function User(name) {
    this.name = name;
}

// Method stored once in prototype
User.prototype.greet = function() {
    console.log(`Hello, I'm ${this.name}`);
};

const user1 = new User("John");
const user2 = new User("Jane");
const user3 = new User("Bob");

// All users share the same greet method
console.log(user1.greet === user2.greet);  // true

// Benefits:
// • One copy of greet() in memory
// • Easy to maintain (change once, affects all)
// • Inheritance mechanism
// • Memory efficient

Benefits Visualization

┌─────────────────────────────────────────────────────────────┐
│         WITHOUT PROTOTYPE vs WITH PROTOTYPE                  │
└─────────────────────────────────────────────────────────────┘

Without Prototype (1000 users):
────────────────────────────────
Memory Usage:
• 1000 user objects
• 1000 copies of greet()
• ~16 KB per function × 1000 = 16 MB wasted!

With Prototype (1000 users):
─────────────────────────────
Memory Usage:
• 1000 user objects
• 1 copy of greet() in prototype
• ~16 KB total for function
• Saves ~15.98 MB!

Performance:
────────────
• Faster object creation
• Less garbage collection
• Better memory locality
• Easier maintenance

3. Real-World Use Cases

1. Framework/Library Development

// jQuery-like library
function $(selector) {
    this.elements = document.querySelectorAll(selector);
}

// All methods shared via prototype
$.prototype.hide = function() {
    this.elements.forEach(el => el.style.display = 'none');
    return this;  // Chaining
};

$.prototype.show = function() {
    this.elements.forEach(el => el.style.display = 'block');
    return this;
};

$.prototype.addClass = function(className) {
    this.elements.forEach(el => el.classList.add(className));
    return this;
};

// Usage
new $('.button').hide().addClass('hidden');

2. Custom Data Structures

// Custom Stack implementation
function Stack() {
    this.items = [];
}

Stack.prototype.push = function(item) {
    this.items.push(item);
};

Stack.prototype.pop = function() {
    return this.items.pop();
};

Stack.prototype.peek = function() {
    return this.items[this.items.length - 1];
};

Stack.prototype.isEmpty = function() {
    return this.items.length === 0;
};

const stack = new Stack();
stack.push(1);
stack.push(2);
console.log(stack.pop());  // 2

3. Extending Built-in Objects (Carefully!)

// Add utility method to Array prototype
// Note: Generally avoid modifying built-in prototypes
Array.prototype.last = function() {
    return this[this.length - 1];
};

const numbers = [1, 2, 3, 4, 5];
console.log(numbers.last());  // 5

// Better approach: Use utility functions
function last(array) {
    return array[array.length - 1];
}

4. Inheritance Hierarchies

// Base class
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`);
};

// Derived class
function Dog(name, breed) {
    Animal.call(this, name);  // Call parent constructor
    this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add Dog-specific method
Dog.prototype.bark = function() {
    console.log(`${this.name} says Woof!`);
};

const dog = new Dog("Buddy", "Golden Retriever");
dog.eat();   // Inherited from Animal
dog.bark();  // Dog-specific method

4. Syntax

Creating Objects with Prototypes

// ═══════════════════════════════════════════════════════════
// METHOD 1: Constructor Function
// ═══════════════════════════════════════════════════════════

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    return `Hello, I'm ${this.name}`;
};

const person1 = new Person("John", 30);
console.log(person1.greet());  // "Hello, I'm John"

// ═══════════════════════════════════════════════════════════
// METHOD 2: Object.create()
// ═══════════════════════════════════════════════════════════

const personPrototype = {
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

const person2 = Object.create(personPrototype);
person2.name = "Jane";
person2.age = 25;
console.log(person2.greet());  // "Hello, I'm Jane"

// ═══════════════════════════════════════════════════════════
// METHOD 3: ES6 Classes (Syntactic Sugar)
// ═══════════════════════════════════════════════════════════

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

const person3 = new Person("Bob", 35);
console.log(person3.greet());  // "Hello, I'm Bob"

// All three methods create prototype chains!

Accessing Prototypes

// ═══════════════════════════════════════════════════════════
// ACCESSING PROTOTYPE
// ═══════════════════════════════════════════════════════════

const obj = {};

// Method 1: Object.getPrototypeOf() (Recommended)
const proto1 = Object.getPrototypeOf(obj);
console.log(proto1 === Object.prototype);  // true

// Method 2: __proto__ (Deprecated but widely supported)
const proto2 = obj.__proto__;
console.log(proto2 === Object.prototype);  // true

// Method 3: constructor.prototype (for instances)
function User() {}
const user = new User();
console.log(user.constructor.prototype === User.prototype);  // true

// ═══════════════════════════════════════════════════════════
// SETTING PROTOTYPE
// ═══════════════════════════════════════════════════════════

const parent = { x: 10 };
const child = {};

// Method 1: Object.setPrototypeOf() (Recommended)
Object.setPrototypeOf(child, parent);
console.log(child.x);  // 10

// Method 2: Object.create() (Best for new objects)
const child2 = Object.create(parent);
console.log(child2.x);  // 10

// Method 3: __proto__ (Avoid)
const child3 = {};
child3.__proto__ = parent;
console.log(child3.x);  // 10

Checking Prototype Relationships

// ═══════════════════════════════════════════════════════════
// PROTOTYPE CHECKS
// ═══════════════════════════════════════════════════════════

function User(name) {
    this.name = name;
}

const user = new User("John");

// Check if object is in prototype chain
console.log(User.prototype.isPrototypeOf(user));  // true
console.log(Object.prototype.isPrototypeOf(user));  // true

// Check if property exists on object (not prototype)
console.log(user.hasOwnProperty('name'));  // true
console.log(user.hasOwnProperty('toString'));  // false

// instanceof operator
console.log(user instanceof User);  // true
console.log(user instanceof Object);  // true

// Get all property names (including prototype)
console.log(Object.getOwnPropertyNames(user));  // ['name']
console.log(Object.getOwnPropertyNames(User.prototype));  // ['constructor']

5. Internal Working

Property Lookup Mechanism

┌─────────────────────────────────────────────────────────────┐
│         PROPERTY LOOKUP ALGORITHM                            │
└─────────────────────────────────────────────────────────────┘

When accessing obj.property:

1. Check if property exists on obj itself
   ↓
   Found? → Return value
   ↓
   Not found? → Continue to step 2

2. Get obj's [[Prototype]]
   ↓
   Is it null? → Return undefined
   ↓
   Not null? → Check if property exists on prototype
   ↓
   Found? → Return value
   ↓
   Not found? → Set obj = prototype, go to step 2

This continues until:
• Property is found, OR
• Prototype chain ends (null)

Example:
────────
const obj = { a: 1 };
obj.toString();

Lookup:
1. obj.toString? No
2. obj.[[Prototype]].toString? Yes! (Object.prototype.toString)
3. Return Object.prototype.toString

Constructor Function Execution

┌─────────────────────────────────────────────────────────────┐
│         new OPERATOR EXECUTION STEPS                         │
└─────────────────────────────────────────────────────────────┘

const user = new User("John");

Step 1: Create new empty object
────────────────────────────────
const newObj = {};

Step 2: Set prototype
─────────────────────
Object.setPrototypeOf(newObj, User.prototype);
// or: newObj.__proto__ = User.prototype;

Step 3: Execute constructor with 'this' = newObj
────────────────────────────────────────────────
User.call(newObj, "John");
// Inside User: this.name = "John"

Step 4: Return object
──────────────────────
return newObj;  // Unless constructor explicitly returns object

Result:
───────
newObj = {
    name: "John",
    [[Prototype]]: User.prototype
}

Prototype Chain Traversal

// Example object hierarchy
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log("Eating...");
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
    console.log("Woof!");
};

const dog = new Dog("Buddy", "Golden Retriever");

// Property lookup for dog.bark()
// 1. dog.bark? Yes! → Execute
dog.bark();

// Property lookup for dog.eat()
// 1. dog.eat? No
// 2. Dog.prototype.eat? No
// 3. Animal.prototype.eat? Yes! → Execute
dog.eat();

// Property lookup for dog.toString()
// 1. dog.toString? No
// 2. Dog.prototype.toString? No
// 3. Animal.prototype.toString? No
// 4. Object.prototype.toString? Yes! → Execute
dog.toString();

6. Memory Diagram

┌─────────────────────────────────────────────────────────────┐
│         PROTOTYPE CHAIN MEMORY LAYOUT                        │
└─────────────────────────────────────────────────────────────┘

Code:
─────
function User(name) {
    this.name = name;
}

User.prototype.greet = function() {
    console.log(`Hello, ${this.name}`);
};

const user1 = new User("John");
const user2 = new User("Jane");

Memory Layout:
──────────────

HEAP MEMORY:
┌──────────────────────────────────────────────────────────┐
│                                                           │
│  User Function Object                                     │
│  ┌─────────────────────────────────────────────────┐    │
│  │ prototype: 0x1000 ────────────────────┐         │    │
│  └─────────────────────────────────────────────────┘    │
│                                           │               │
│                                           ↓               │
│  User.prototype (0x1000)                                  │
│  ┌─────────────────────────────────────────────────┐    │
│  │ constructor: User                                │    │
│  │ greet: function() {...}                          │    │
│  │ [[Prototype]]: Object.prototype                  │    │
│  └─────────────────────────────────────────────────┘    │
│         ↑                    ↑                            │
│         │                    │                            │
│  ┌──────┴──────┐      ┌──────┴──────┐                   │
│  │             │      │             │                    │
│  user1 (0x2000)      user2 (0x3000)                      │
│  ┌─────────────┐      ┌─────────────┐                   │
│  │ name: "John"│      │ name: "Jane"│                   │
│  │ [[Prototype]]      │ [[Prototype]]                   │
│  │    ↓         │      │    ↓         │                   │
│  │  0x1000     │      │  0x1000     │                   │
│  └─────────────┘      └─────────────┘                   │
│                                                           │
│  Object.prototype (0x4000)                                │
│  ┌─────────────────────────────────────────────────┐    │
│  │ toString: function() {...}                       │    │
│  │ valueOf: function() {...}                        │    │
│  │ hasOwnProperty: function() {...}                 │    │
│  │ [[Prototype]]: null                              │    │
│  └─────────────────────────────────────────────────┘    │
│                                                           │
└──────────────────────────────────────────────────────────┘

Key Points:
───────────
• user1 and user2 share the same prototype (0x1000)
• Only one copy of greet() in memory
• Each user has its own 'name' property
• All objects eventually link to Object.prototype

Memory Comparison

┌─────────────────────────────────────────────────────────────┐
│         MEMORY USAGE COMPARISON                              │
└─────────────────────────────────────────────────────────────┘

Scenario: 1000 User objects with 5 methods

Without Prototype:
──────────────────
• 1000 user objects: ~24 KB
• 5000 function objects (5 × 1000): ~80 MB
• Total: ~80 MB

With Prototype:
───────────────
• 1000 user objects: ~24 KB
• 5 function objects (shared): ~80 KB
• Total: ~104 KB

Savings: ~79.9 MB (99.87% reduction!)

7. Data Flow Diagram

┌─────────────────────────────────────────────────────────────┐
│         PROTOTYPE CHAIN LOOKUP FLOW                          │
└─────────────────────────────────────────────────────────────┘

user.greet()
     ↓
┌─────────────────────────────────────┐
│  1. Check user object                │
│     Has 'greet' property?            │
└─────────────────────────────────────┘
     ↓
    No
     ↓
┌─────────────────────────────────────┐
│  2. Get user.[[Prototype]]           │
│     (User.prototype)                 │
└─────────────────────────────────────┘
     ↓
┌─────────────────────────────────────┐
│  3. Check User.prototype             │
│     Has 'greet' property?            │
└─────────────────────────────────────┘
     ↓
   Yes! ✓
     ↓
┌─────────────────────────────────────┐
│  4. Execute greet() with             │
│     this = user                      │
└─────────────────────────────────────┘
     ↓
   Result


user.toString()
     ↓
┌─────────────────────────────────────┐
│  1. Check user object                │
│     Has 'toString' property?         │
└─────────────────────────────────────┘
     ↓
    No
     ↓
┌─────────────────────────────────────┐
│  2. Check User.prototype             │
│     Has 'toString' property?         │
└─────────────────────────────────────┘
     ↓
    No
     ↓
┌─────────────────────────────────────┐
│  3. Check Object.prototype           │
│     Has 'toString' property?         │
└─────────────────────────────────────┘
     ↓
   Yes! ✓
     ↓
┌─────────────────────────────────────┐
│  4. Execute toString() with          │
│     this = user                      │
└─────────────────────────────────────┘
     ↓
   Result


user.nonExistent
     ↓
┌─────────────────────────────────────┐
│  1. Check user object                │
└─────────────────────────────────────┘
     ↓ No
┌─────────────────────────────────────┐
│  2. Check User.prototype             │
└─────────────────────────────────────┘
     ↓ No
┌─────────────────────────────────────┐
│  3. Check Object.prototype           │
└─────────────────────────────────────┘
     ↓ No
┌─────────────────────────────────────┐
│  4. Check null (end of chain)        │
└─────────────────────────────────────┘
     ↓
  undefined

8. Code Examples

Example 1: Basic Prototype Chain

// Create constructor
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Add method to prototype
Person.prototype.introduce = function() {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
};

Person.prototype.birthday = function() {
    this.age++;
    console.log(`Happy birthday! Now ${this.age} years old.`);
};

// Create instances
const john = new Person("John", 30);
const jane = new Person("Jane", 25);

// Use methods
console.log(john.introduce());  // "Hi, I'm John and I'm 30 years old."
jane.birthday();  // "Happy birthday! Now 26 years old."

// Verify shared prototype
console.log(john.introduce === jane.introduce);  // true (same function)
console.log(john.hasOwnProperty('name'));  // true
console.log(john.hasOwnProperty('introduce'));  // false (on prototype)

Example 2: Inheritance Chain

// Base class
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`);
};

Animal.prototype.sleep = function() {
    console.log(`${this.name} is sleeping`);
};

// Derived class
function Dog(name, breed) {
    Animal.call(this, name);  // Call parent constructor
    this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add Dog-specific methods
Dog.prototype.bark = function() {
    console.log(`${this.name} says Woof!`);
};

Dog.prototype.fetch = function(item) {
    console.log(`${this.name} fetched the ${item}`);
};

// Create instance
const buddy = new Dog("Buddy", "Golden Retriever");

// Use inherited methods
buddy.eat();    // "Buddy is eating" (from Animal)
buddy.sleep();  // "Buddy is sleeping" (from Animal)

// Use Dog-specific methods
buddy.bark();   // "Buddy says Woof!"
buddy.fetch("ball");  // "Buddy fetched the ball"

// Check prototype chain
console.log(buddy instanceof Dog);     // true
console.log(buddy instanceof Animal);  // true
console.log(buddy instanceof Object);  // true

Example 3: Object.create() Pattern

// Define prototype object
const vehiclePrototype = {
    start() {
        console.log(`${this.make} ${this.model} is starting`);
    },
    stop() {
        console.log(`${this.make} ${this.model} is stopping`);
    },
    drive(distance) {
        console.log(`Driving ${distance} miles`);
        this.mileage += distance;
    }
};

// Factory function
function createVehicle(make, model, year) {
    const vehicle = Object.create(vehiclePrototype);
    vehicle.make = make;
    vehicle.model = model;
    vehicle.year = year;
    vehicle.mileage = 0;
    return vehicle;
}

// Create vehicles
const car1 = createVehicle("Toyota", "Camry", 2020);
const car2 = createVehicle("Honda", "Accord", 2021);

// Use methods
car1.start();  // "Toyota Camry is starting"
car1.drive(100);  // "Driving 100 miles"
console.log(car1.mileage);  // 100

// Verify prototype
console.log(Object.getPrototypeOf(car1) === vehiclePrototype);  // true
console.log(car1.start === car2.start);  // true (shared method)

Example 4: Extending Built-in Prototypes (Carefully!)

// Add utility method to Array prototype
// Note: Generally avoid this in production code
Array.prototype.shuffle = function() {
    const array = [...this];
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
};

Array.prototype.unique = function() {
    return [...new Set(this)];
};

// Usage
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.shuffle());  // [3, 1, 5, 2, 4] (random)

const duplicates = [1, 2, 2, 3, 3, 3, 4];
console.log(duplicates.unique());  // [1, 2, 3, 4]

// Better approach: Use utility functions
function shuffle(array) {
    const result = [...array];
    for (let i = result.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [result[i], result[j]] = [result[j], result[i]];
    }
    return result;
}

Example 5: Multiple Inheritance (Mixin Pattern)

// Mixins
const canEat = {
    eat(food) {
        console.log(`${this.name} is eating ${food}`);
    }
};

const canWalk = {
    walk() {
        console.log(`${this.name} is walking`);
    }
};

const canSwim = {
    swim() {
        console.log(`${this.name} is swimming`);
    }
};

// Base class
function Animal(name) {
    this.name = name;
}

// Apply mixins
Object.assign(Animal.prototype, canEat, canWalk);

// Derived class with additional mixin
function Duck(name) {
    Animal.call(this, name);
}

Duck.prototype = Object.create(Animal.prototype);
Duck.prototype.constructor = Duck;
Object.assign(Duck.prototype, canSwim);

// Create instance
const donald = new Duck("Donald");

// Use mixed-in methods
donald.eat("bread");  // "Donald is eating bread"
donald.walk();        // "Donald is walking"
donald.swim();        // "Donald is swimming"

9. Common Mistakes

Mistake 1: Confusing prototype and __proto__

// ❌ Wrong: Confusing the two
function User(name) {
    this.name = name;
}

const user = new User("John");

// These are DIFFERENT!
console.log(User.prototype);  // Object with constructor and methods
console.log(user.__proto__);  // Same as User.prototype
console.log(user.prototype);  // undefined (instances don't have prototype)

// ✅ Correct understanding:
// • prototype: Property on constructor functions
// • __proto__: Internal link on objects (use Object.getPrototypeOf())

console.log(user.__proto__ === User.prototype);  // true
console.log(Object.getPrototypeOf(user) === User.prototype);  // true

Mistake 2: Modifying Built-in Prototypes

// ❌ Wrong: Modifying built-in prototypes
Array.prototype.first = function() {
    return this[0];
};

// Problems:
// • Conflicts with future JavaScript features
// • Affects all arrays globally
// • Can break third-party libraries
// • Non-standard behavior

// ✅ Correct: Use utility functions
function first(array) {
    return array[0];
}

// Or use existing methods
const arr = [1, 2, 3];
console.log(arr[0]);  // Direct access
console.log(arr.at(0));  // ES2022 method

Mistake 3: Forgetting to Reset Constructor

// ❌ Wrong: Not resetting constructor
function Animal() {}
function Dog() {}

Dog.prototype = Object.create(Animal.prototype);
// Forgot to reset constructor!

const dog = new Dog();
console.log(dog.constructor === Dog);  // false (wrong!)
console.log(dog.constructor === Animal);  // true (unexpected!)

// ✅ Correct: Reset constructor
function Cat() {}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;  // Reset constructor

const cat = new Cat();
console.log(cat.constructor === Cat);  // true (correct!)

Mistake 4: Adding Properties to Prototype Instead of Instance

// ❌ Wrong: Mutable properties on prototype
function User(name) {
    this.name = name;
}

User.prototype.friends = [];  // Shared array!

const user1 = new User("John");
const user2 = new User("Jane");

user1.friends.push("Bob");
console.log(user2.friends);  // ["Bob"] (unexpected!)

// ✅ Correct: Instance properties
function User2(name) {
    this.name = name;
    this.friends = [];  // Each instance gets own array
}

const user3 = new User2("John");
const user4 = new User2("Jane");

user3.friends.push("Bob");
console.log(user4.friends);  // [] (correct!)

Mistake 5: Deep Prototype Chains

// ❌ Wrong: Too many levels
function A() {}
function B() {}
function C() {}
function D() {}
function E() {}

B.prototype = Object.create(A.prototype);
C.prototype = Object.create(B.prototype);
D.prototype = Object.create(C.prototype);
E.prototype = Object.create(D.prototype);

const e = new E();
// Property lookup goes through 5+ levels!
// Slow and hard to debug

// ✅ Correct: Keep inheritance shallow
// Prefer composition over deep inheritance
function Component() {
    this.renderer = new Renderer();
    this.eventHandler = new EventHandler();
}

10. Performance Considerations

1. Prototype Lookup Cost

// Property lookup is O(n) where n = chain depth

// ❌ Slow: Deep chain
function A() {}
function B() {}
function C() {}
B.prototype = Object.create(A.prototype);
C.prototype = Object.create(B.prototype);

const c = new C();
c.someMethod();  // Searches through 3+ prototypes

// ✅ Fast: Shallow chain or cache
function D() {
    // Cache frequently accessed prototype methods
    this.cachedMethod = D.prototype.someMethod;
}

2. Own Properties vs Prototype Properties

// Benchmark: hasOwnProperty vs prototype lookup
const obj = { a: 1, b: 2, c: 3 };

// Fast: Own property
console.time('own');
for (let i = 0; i < 1000000; i++) {
    obj.a;
}
console.timeEnd('own');  // ~3ms

// Slower: Prototype property
console.time('prototype');
for (let i = 0; i < 1000000; i++) {
    obj.toString;
}
console.timeEnd('prototype');  // ~5ms

// Optimization: Cache prototype methods
const toString = obj.toString;
console.time('cached');
for (let i = 0; i < 1000000; i++) {
    toString;
}
console.timeEnd('cached');  // ~2ms

3. Object.create() vs Constructor

// Object.create() is slightly faster for simple objects

// Benchmark
console.time('constructor');
for (let i = 0; i < 100000; i++) {
    const obj = new Object();
}
console.timeEnd('constructor');  // ~5ms

console.time('create');
for (let i = 0; i < 100000; i++) {
    const obj = Object.create(Object.prototype);
}
console.timeEnd('create');  // ~4ms

// But constructor is more flexible

4. Avoid Changing Prototypes at Runtime

// ❌ Slow: Changing prototype after creation
function User(name) {
    this.name = name;
}

const user = new User("John");

// This deoptimizes the object!
Object.setPrototypeOf(user, {});

// ✅ Fast: Set prototype once during creation
const user2 = Object.create(User.prototype);
user2.name = "Jane";

11. Interview Questions

Q1: What is the prototype chain?

Answer: The prototype chain is JavaScript's mechanism for inheritance. Every object has an internal link ([[Prototype]]) to another object called its prototype. When accessing a property, JavaScript searches the object, then its prototype, then the prototype's prototype, and so on until reaching null.

const obj = { a: 1 };
console.log(obj.toString());  // Found in Object.prototype

// Chain: obj → Object.prototype → null

Q2: What's the difference between prototype and __proto__?

Answer:

  • prototype: Property on constructor functions, used when creating objects with new
  • __proto__: Accessor property on objects, provides access to the object's [[Prototype]]
function User() {}
const user = new User();

console.log(User.prototype);  // Object with constructor
console.log(user.__proto__);  // Same as User.prototype
console.log(user.__proto__ === User.prototype);  // true

Q3: How does property lookup work?

Answer: JavaScript searches for properties in this order:

  1. Own properties of the object
  2. Properties in the object's prototype
  3. Properties in the prototype's prototype
  4. Continue until reaching null
  5. Return undefined if not found
const obj = { a: 1 };
obj.b;  // 1. Check obj → not found
        // 2. Check Object.prototype → not found
        // 3. Check null → return undefined

Q4: How do you create inheritance in JavaScript?

Answer: Multiple ways:

// Method 1: Constructor + prototype
function Animal(name) {
    this.name = name;
}
Animal.prototype.eat = function() {};

function Dog(name) {
    Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Method 2: Object.create()
const animal = { eat() {} };
const dog = Object.create(animal);

// Method 3: ES6 Classes
class Animal {
    constructor(name) {
        this.name = name;
    }
    eat() {}
}
class Dog extends Animal {}

Q5: What is Object.create()?

Answer: Object.create() creates a new object with the specified prototype object.

const proto = {
    greet() {
        console.log("Hello");
    }
};

const obj = Object.create(proto);
obj.greet();  // "Hello" (from prototype)

// Equivalent to:
const obj2 = {};
Object.setPrototypeOf(obj2, proto);

Q6: Why avoid modifying built-in prototypes?

Answer: Modifying built-in prototypes can:

  • Conflict with future JavaScript features
  • Break third-party libraries
  • Cause unexpected behavior globally
  • Make code harder to maintain
// ❌ Bad
Array.prototype.first = function() {
    return this[0];
};

// ✅ Good
function first(array) {
    return array[0];
}

Q7: What happens when you use new?

Answer: The new operator:

  1. Creates a new empty object
  2. Sets the object's [[Prototype]] to constructor's prototype
  3. Executes constructor with this = new object
  4. Returns the object (unless constructor returns an object)
function User(name) {
    this.name = name;
}

const user = new User("John");

// Equivalent to:
const user2 = Object.create(User.prototype);
User.call(user2, "John");

Q8: How do you check if a property is own or inherited?

Answer: Use hasOwnProperty():

const obj = { a: 1 };

console.log(obj.hasOwnProperty('a'));  // true (own)
console.log(obj.hasOwnProperty('toString'));  // false (inherited)

// Or use Object.hasOwn() (ES2022)
console.log(Object.hasOwn(obj, 'a'));  // true

12. Cheat Sheet

Quick Reference

// ═══════════════════════════════════════════════════════════
// CREATING OBJECTS WITH PROTOTYPES
// ═══════════════════════════════════════════════════════════

// Constructor function
function User(name) {
    this.name = name;
}
User.prototype.greet = function() {};
const user = new User("John");

// Object.create()
const proto = { greet() {} };
const obj = Object.create(proto);

// ES6 Class
class User {
    constructor(name) { this.name = name; }
    greet() {}
}

// ═══════════════════════════════════════════════════════════
// ACCESSING PROTOTYPES
// ═══════════════════════════════════════════════════════════

Object.getPrototypeOf(obj)           // Get prototype (recommended)
obj.__proto__                        // Get prototype (deprecated)
Object.setPrototypeOf(obj, proto)    // Set prototype
obj.constructor.prototype            // Get constructor's prototype

// ═══════════════════════════════════════════════════════════
// CHECKING PROTOTYPES
// ═══════════════════════════════════════════════════════════

proto.isPrototypeOf(obj)             // Check if proto in chain
obj instanceof Constructor           // Check if instance of
obj.hasOwnProperty('prop')           // Check if own property
Object.hasOwn(obj, 'prop')           // ES2022 alternative

// ═══════════════════════════════════════════════════════════
// INHERITANCE
// ═══════════════════════════════════════════════════════════

// Set up inheritance
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// Call parent constructor
function Child() {
    Parent.call(this);
}

// ═══════════════════════════════════════════════════════════
// COMMON PATTERNS
// ═══════════════════════════════════════════════════════════

// Singleton
const singleton = {
    method() {}
};

// Factory
function createUser(name) {
    return Object.create(userProto, {
        name: { value: name }
    });
}

// Mixin
Object.assign(Target.prototype, mixin1, mixin2);

Prototype Chain Visualization

┌─────────────────────────────────────────────────────────────┐
│         COMPLETE PROTOTYPE CHAIN                             │
└─────────────────────────────────────────────────────────────┘

const arr = [1, 2, 3];

arr
 ↓ [[Prototype]]
Array.prototype
 ↓ [[Prototype]]
Object.prototype
 ↓ [[Prototype]]
null

Methods available:
──────────────────
arr.push()          // From Array.prototype
arr.toString()      // From Object.prototype
arr.hasOwnProperty()// From Object.prototype

13. Summary

Key Takeaways

Prototype Chain - JavaScript's inheritance mechanism
Every object has a prototype - Linked via [[Prototype]]
Property lookup - Searches object → prototype → ... → null
prototype property - On constructor functions
__proto__ accessor - On objects (use Object.getPrototypeOf())
Memory efficient - Methods shared via prototype
Inheritance - Via Object.create() or constructor functions
ES6 Classes - Syntactic sugar over prototypes

Best Practices

  1. Use Object.getPrototypeOf() instead of __proto__
  2. Don't modify built-in prototypes in production
  3. Keep inheritance shallow (prefer composition)
  4. Reset constructor after setting prototype
  5. Put methods on prototype, data on instance
  6. Use ES6 classes for cleaner syntax
  7. Cache prototype methods for performance
  8. Use hasOwnProperty() to check own properties
  9. Use Object.assign() to shallow copy properties
  10. Use Object.create() to create objects with custom prototypes