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
- What is the Prototype Chain?
- Why Do We Need the Prototype Chain?
- Real-World Use Cases
- Syntax
- Internal Working
- Memory Diagram
- Data Flow Diagram
- Code Examples
- Common Mistakes
- Performance Considerations
- Interview Questions
- Cheat Sheet
- 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 withnew__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:
- Own properties of the object
- Properties in the object's prototype
- Properties in the prototype's prototype
- Continue until reaching
null - Return
undefinedif 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:
- Creates a new empty object
- Sets the object's
[[Prototype]]to constructor'sprototype - Executes constructor with
this= new object - 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
- Use
Object.getPrototypeOf()instead of__proto__ - Don't modify built-in prototypes in production
- Keep inheritance shallow (prefer composition)
- Reset constructor after setting prototype
- Put methods on prototype, data on instance
- Use ES6 classes for cleaner syntax
- Cache prototype methods for performance
- Use
hasOwnProperty()to check own properties - Use
Object.assign()to shallow copy properties - Use
Object.create()to create objects with custom prototypes