JavaScript prototypes are the mechanism for inheritance and object property lookup. This guide explains prototypes, the prototype chain, and how inheritance works.
What is a Prototype? #
Every JavaScript object has an internal property [[Prototype]] that references another object. This creates a chain used for property lookup.
const obj = { name: 'Alice' };
// Access prototype
console.log(Object.getPrototypeOf(obj)); // Object.prototype
// Check if property exists on object (not prototype)
console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('toString')); // false
Prototype Chain #
When accessing a property, JavaScript searches:
- The object itself
- The object’s prototype
- The prototype’s prototype
- Until reaching
null
const animal = {
eats: true,
walk() {
console.log('Walking');
}
};
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.eats); // true (from animal prototype)
console.log(dog.barks); // true (own property)
// Prototype chain: dog → animal → Object.prototype → null
Constructor Functions #
Functions can create objects with shared prototypes.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
Person.prototype.getAge = function() {
return this.age;
};
const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);
console.log(alice.greet()); // Hello, I'm Alice
console.log(bob.greet()); // Hello, I'm Bob
// Both share same prototype
console.log(alice.greet === bob.greet); // true
What ’new’ Does #
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
// Equivalent to:
const alice = {};
Object.setPrototypeOf(alice, Person.prototype);
Person.call(alice, 'Alice');Object.create() #
Create objects with specific prototype.
const personPrototype = {
greet() {
return `Hello, I'm ${this.name}`;
},
getAge() {
return this.age;
}
};
const alice = Object.create(personPrototype);
alice.name = 'Alice';
alice.age = 25;
console.log(alice.greet()); // Hello, I'm Alice
Object.create() vs Constructor #
// Constructor function
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const alice = new Person('Alice');
// Equivalent with Object.create
const personPrototype = {
greet() {
return `Hello, ${this.name}`;
}
};
function createPerson(name) {
const person = Object.create(personPrototype);
person.name = name;
return person;
}
const bob = createPerson('Bob');Prototype Properties #
prototype vs proto #
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
// Function's prototype property (blueprint)
console.log(Person.prototype);
// Object's actual prototype (__proto__ or [[Prototype]])
console.log(alice.__proto__ === Person.prototype); // true
// Better way to access prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
Constructor Property #
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.constructor === Person); // true
console.log(Person.prototype.constructor === Person); // true
// Can create new instances
const bob = new alice.constructor('Bob');Prototypal Inheritance #
Basic Inheritance #
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a 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;
// Add Dog methods
Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};
const dog = new Dog('Max', 'Golden Retriever');
dog.speak(); // Max makes a sound
dog.bark(); // Max barks
Multiple Levels of Inheritance #
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Mammal(name) {
Animal.call(this, name);
}
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.warmBlooded = function() {
console.log(`${this.name} is warm-blooded`);
};
function Dog(name, breed) {
Mammal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Mammal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};
const dog = new Dog('Max', 'Labrador');
dog.eat(); // Max is eating (from Animal)
dog.warmBlooded(); // Max is warm-blooded (from Mammal)
dog.bark(); // Max barks (from Dog)
Checking Prototypes #
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
// Check if prototype exists in chain
console.log(Person.prototype.isPrototypeOf(alice)); // true
console.log(Object.prototype.isPrototypeOf(alice)); // true
// instanceof
console.log(alice instanceof Person); // true
console.log(alice instanceof Object); // true
// Get prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
// Has own property
console.log(alice.hasOwnProperty('name')); // true
console.log(alice.hasOwnProperty('greet')); // false
Modifying Prototypes #
Adding Methods #
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
// Add method to prototype
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
// Existing instances get new method
console.log(alice.greet()); // Hello, I'm Alice
Extending Built-in Prototypes #
// Add method to Array prototype
Array.prototype.last = function() {
return this[this.length - 1];
};
const arr = [1, 2, 3, 4, 5];
console.log(arr.last()); // 5
// WARNING: Generally avoid modifying built-in prototypes
// Can cause conflicts with future JavaScript versions
Object Property Enumeration #
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const alice = new Person('Alice', 25);
// for...in loops through own and inherited properties
for (let key in alice) {
console.log(key); // name, age, greet
}
// Only own properties
for (let key in alice) {
if (alice.hasOwnProperty(key)) {
console.log(key); // name, age
}
}
// Object.keys - only own enumerable properties
console.log(Object.keys(alice)); // ['name', 'age']
// Object.getOwnPropertyNames - all own properties
console.log(Object.getOwnPropertyNames(alice)); // ['name', 'age']
Prototype vs Class #
Modern JavaScript classes are syntactic sugar over prototypes.
// Class syntax
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// Equivalent with prototypes
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};Class Inheritance #
// Classes
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
// Prototypes
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
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(`${this.name} barks`);
};Practical Examples #
Mixin Pattern #
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`);
}
};
function Person(name) {
this.name = name;
}
// Mix in abilities
Object.assign(Person.prototype, canEat, canWalk);
function Fish(name) {
this.name = name;
}
Object.assign(Fish.prototype, canEat, canSwim);
const alice = new Person('Alice');
alice.eat('pizza'); // Alice is eating pizza
alice.walk(); // Alice is walking
const nemo = new Fish('Nemo');
nemo.eat('plankton'); // Nemo is eating plankton
nemo.swim(); // Nemo is swimming
Method Delegation #
const arrayMethods = {
push(item) {
this.items.push(item);
console.log(`Added ${item}`);
},
pop() {
const item = this.items.pop();
console.log(`Removed ${item}`);
return item;
},
get length() {
return this.items.length;
}
};
function Stack() {
this.items = [];
}
Object.setPrototypeOf(Stack.prototype, arrayMethods);
const stack = new Stack();
stack.push(1); // Added 1
stack.push(2); // Added 2
stack.pop(); // Removed 2
Prototype-based Factory #
const vehiclePrototype = {
start() {
console.log(`${this.name} started`);
},
stop() {
console.log(`${this.name} stopped`);
}
};
function createVehicle(name, type) {
const vehicle = Object.create(vehiclePrototype);
vehicle.name = name;
vehicle.type = type;
return vehicle;
}
const car = createVehicle('Toyota', 'car');
const bike = createVehicle('Honda', 'motorcycle');
car.start(); // Toyota started
bike.start(); // Honda started
Performance Considerations #
Shared Methods #
// Bad - New function for each instance
function Person(name) {
this.name = name;
this.greet = function() {
return `Hello, ${this.name}`;
};
}
const alice = new Person('Alice');
const bob = new Person('Bob');
console.log(alice.greet === bob.greet); // false (wasteful)
// Good - Shared on prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const alice = new Person('Alice');
const bob = new Person('Bob');
console.log(alice.greet === bob.greet); // true (efficient)
Property Lookup #
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const alice = new Person('Alice');
// Fast - Own property
console.log(alice.name);
// Slower - Prototype lookup
console.log(alice.greet);
// Cache if accessed frequently
const greet = alice.greet;
console.log(greet.call(alice));Common Pitfalls #
Losing ’this’ Context #
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const alice = new Person('Alice');
const greet = alice.greet;
console.log(greet()); // Hello, undefined
// Fix with bind
const boundGreet = alice.greet.bind(alice);
console.log(boundGreet()); // Hello, Alice
Overwriting Prototype #
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return 'Hello';
};
const alice = new Person('Alice');
// Bad - Breaks existing instances
Person.prototype = {
greet() {
return 'Hi';
}
};
console.log(alice.greet()); // Still "Hello"
const bob = new Person('Bob');
console.log(bob.greet()); // "Hi"
Prototype Pollution #
// Dangerous - Can affect all objects
Object.prototype.polluted = true;
const obj = {};
console.log(obj.polluted); // true
// Avoid modifying Object.prototype
delete Object.prototype.polluted;Modern Alternatives #
Object.setPrototypeOf #
const animal = {
speak() {
console.log('Sound');
}
};
const dog = {
bark() {
console.log('Woof');
}
};
Object.setPrototypeOf(dog, animal);
dog.bark(); // Woof
dog.speak(); // Sound
Object.assign #
const defaults = {
theme: 'light',
language: 'en'
};
const userSettings = Object.assign({}, defaults, {
theme: 'dark'
});
console.log(userSettings);
// { theme: 'dark', language: 'en' }
Best Practices #
- Use classes for new code - Cleaner syntax
- Don’t modify built-in prototypes - Can cause conflicts
- Put methods on prototype - Memory efficient
- Use Object.create for inheritance - Clear intent
- Check with hasOwnProperty - Distinguish own properties
- Prefer composition - More flexible than inheritance
- Use getPrototypeOf - Instead of proto
- Document inheritance chains - Help other developers
Understanding prototypes is essential for mastering JavaScript. While classes provide cleaner syntax, knowing prototypes helps you understand what’s happening under the hood.