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