Explain the concept of prototypes in JavaScript.
Prototypes are a fundamental concept in JavaScript that form the basis of JavaScript’s object-oriented programming model. Unlike classical inheritance found in languages like Java or C#, JavaScript uses prototypal inheritance, where objects can inherit properties and methods directly from other objects through a prototype chain.
Prototype-Based Inheritance
In JavaScript, every object has an internal property called [[Prototype]]
(exposed through __proto__
in most browsers), which references another object called its prototype. When you try to access a property or method on an object, JavaScript first looks for it directly on the object. If it doesn’t find it there, it looks up the prototype chain until it either finds the property or reaches the end of the chain (typically Object.prototype
).
Object.prototype
At the top of the prototype chain is Object.prototype
, which provides common methods like toString()
, valueOf()
, and hasOwnProperty()
that are available to all JavaScript objects.
const obj = {};
console.log(obj.toString()); // "[object Object]"
console.log(obj.__proto__ === Object.prototype); // true
Constructor Functions and Prototypes
Before ES6 classes, constructor functions were the primary way to create objects with shared properties and methods:
// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
// Adding a method to the prototype
Person.prototype.greet = function() {
return `Hello, my name is ${this.name}`;
};
// Creating instances
const person1 = new Person('Rahul', 28);
const person2 = new Person('Priya', 25);
console.log(person1.greet()); // "Hello, my name is Rahul"
console.log(person2.greet()); // "Hello, my name is Priya"
// Both instances share the same greet method
console.log(person1.greet === person2.greet); // true
In this example:
Person
is a constructor functionPerson.prototype
is an object that contains shared methods- When we create instances with
new Person()
, the objects inherit fromPerson.prototype
- The
greet
method is defined once but accessible by all instances
The Prototype Chain
The prototype chain is a series of linked objects that enables inheritance in JavaScript:
// Create a base object
const animal = {
eats: true,
sleep() {
return 'Sleeping...';
}
};
// Create an object with animal as its prototype
const rabbit = Object.create(animal);
rabbit.jumps = true;
// Create an object with rabbit as its prototype
const whiteRabbit = Object.create(rabbit);
whiteRabbit.color = 'white';
console.log(whiteRabbit.jumps); // true (inherited from rabbit)
console.log(whiteRabbit.eats); // true (inherited from animal)
console.log(whiteRabbit.sleep()); // "Sleeping..." (inherited from animal)
// The prototype chain:
// whiteRabbit ---> rabbit ---> animal ---> Object.prototype ---> null
Accessing and Modifying Prototypes
Getting an Object’s Prototype
// Modern way (recommended)
Object.getPrototypeOf(obj);
// Legacy way (not recommended for production)
obj.__proto__;
Setting an Object’s Prototype
// During object creation
const child = Object.create(parent);
// After creation (ES6+)
Object.setPrototypeOf(child, parent); // Not recommended for performance reasons
Checking Prototype Relationships
// Check if an object is in another object's prototype chain
console.log(Object.prototype.isPrototypeOf(obj)); // true for any object
// Check if an object was created with a specific constructor
console.log(obj instanceof Constructor);
Property Shadowing
When you add a property to an object that already exists in its prototype, you’re not modifying the prototype - you’re creating a new property that shadows the prototype’s property:
function Animal() {}
Animal.prototype.legs = 4;
const cat = new Animal();
console.log(cat.legs); // 4 (from prototype)
cat.legs = 3; // Create a new property on cat
console.log(cat.legs); // 3 (own property)
console.log(Animal.prototype.legs); // 4 (unchanged)
// Check if property is on the object itself or its prototype
console.log(cat.hasOwnProperty('legs')); // true
Prototype Methods and Properties
Object.create()
Creates a new object with the specified prototype:
const personProto = {
greet() {
return `Hello, my name is ${this.name}`;
}
};
const person = Object.create(personProto);
person.name = 'Rahul';
console.log(person.greet()); // "Hello, my name is Rahul"
Object.getPrototypeOf() and Object.setPrototypeOf()
Get or set the prototype of an object:
const proto = { value: 42 };
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // true
const newProto = { value: 100 };
Object.setPrototypeOf(obj, newProto);
console.log(obj.value); // 100
hasOwnProperty()
Checks if a property is defined directly on the object (not inherited):
const obj = { a: 1 };
const child = Object.create(obj);
child.b = 2;
console.log(child.hasOwnProperty('a')); // false (inherited)
console.log(child.hasOwnProperty('b')); // true (own property)
isPrototypeOf()
Checks if an object exists in another object’s prototype chain:
const proto = {};
const obj = Object.create(proto);
console.log(proto.isPrototypeOf(obj)); // true
console.log(Object.prototype.isPrototypeOf(obj)); // true
Constructor Property
Each function in JavaScript automatically gets a prototype
property, and that prototype object gets a constructor
property that points back to the function:
function Person(name) {
this.name = name;
}
const person = new Person('Rahul');
console.log(Person.prototype.constructor === Person); // true
console.log(person.constructor === Person); // true
// You can use the constructor property to create new instances
const anotherPerson = new person.constructor('Priya');
console.log(anotherPerson.name); // "Priya"
Prototypal Inheritance Patterns
Constructor Pattern
function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function() {
return 'Some generic sound';
};
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; // Fix the constructor property
// Add or override methods
Dog.prototype.makeSound = function() {
return 'Woof!';
};
Dog.prototype.fetch = function() {
return `${this.name} is fetching.`;
};
const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.name); // "Rex"
console.log(dog.breed); // "German Shepherd"
console.log(dog.makeSound()); // "Woof!"
console.log(dog.fetch()); // "Rex is fetching."
Object.create Pattern
const animal = {
init(name) {
this.name = name;
return this;
},
makeSound() {
return 'Some generic sound';
}
};
const dog = Object.create(animal);
dog.init = function(name, breed) {
animal.init.call(this, name);
this.breed = breed;
return this;
};
dog.makeSound = function() {
return 'Woof!';
};
dog.fetch = function() {
return `${this.name} is fetching.`;
};
const myDog = Object.create(dog).init('Rex', 'German Shepherd');
console.log(myDog.makeSound()); // "Woof!"
console.log(myDog.fetch()); // "Rex is fetching."
Mixin Pattern
// Mixin function
function mixin(target, ...sources) {
Object.assign(target, ...sources);
return target;
}
// Base object
function Dog(name) {
this.name = name;
}
// Mixins
const canWalk = {
walk() {
return `${this.name} is walking.`;
}
};
const canSwim = {
swim() {
return `${this.name} is swimming.`;
}
};
// Apply mixins to prototype
mixin(Dog.prototype, canWalk, canSwim);
const dog = new Dog('Rex');
console.log(dog.walk()); // "Rex is walking."
console.log(dog.swim()); // "Rex is swimming."
ES6 Classes and Prototypes
ES6 classes provide a more familiar syntax for creating objects and implementing inheritance, but they’re essentially syntactic sugar over JavaScript’s prototype-based inheritance:
// ES6 Class
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
return 'Some generic sound';
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
makeSound() {
return 'Woof!';
}
fetch() {
return `${this.name} is fetching.`;
}
}
const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.makeSound()); // "Woof!"
Behind the scenes, this is still using prototypes:
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
Prototype Chain Performance
Property lookup through the prototype chain can affect performance, especially in performance-critical code:
// Direct property access (fast)
function DirectCar() {
this.speed = 0;
}
DirectCar.prototype.accelerate = function() {
this.speed += 10;
};
// Prototype chain lookup (slower)
function ProtoCar() {}
ProtoCar.prototype.speed = 0;
ProtoCar.prototype.accelerate = function() {
this.speed += 10; // Creates a new property on the instance
};
Common Prototype Pitfalls
Modifying Built-in Prototypes
Modifying built-in prototypes like Array.prototype
or Object.prototype
is generally considered bad practice:
// Avoid this!
Array.prototype.first = function() {
return this[0];
};
// This affects ALL arrays in your application
const arr = [1, 2, 3];
console.log(arr.first()); // 1
Forgetting to Set the Constructor
When setting up inheritance, forgetting to reset the constructor property can cause issues:
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
// Dog.prototype.constructor = Dog; // Forgot this line
const dog = new Dog();
console.log(dog.constructor); // Animal (incorrect)
Sharing Reference Types on the Prototype
Properties on the prototype are shared among all instances, which can lead to unexpected behavior with reference types:
function Person() {}
Person.prototype.friends = []; // Shared array!
const person1 = new Person();
const person2 = new Person();
person1.friends.push('Alice');
console.log(person2.friends); // ["Alice"] - Affected all instances!
// Better approach
function BetterPerson() {
this.friends = []; // Instance property, not shared
}
Interview Tips
- Explain that prototypes are the foundation of JavaScript’s inheritance model
- Describe how the prototype chain works for property lookup
- Differentiate between own properties and inherited properties
- Explain the relationship between constructor functions, their prototype property, and instances
- Discuss how ES6 classes are syntactic sugar over prototypal inheritance
- Be prepared to demonstrate prototype inheritance with both constructor functions and ES6 classes
- Mention common pitfalls like modifying built-in prototypes or sharing reference types
- Explain the performance implications of the prototype chain
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.