Prototype Chain
Learn how JavaScript's prototype system works and how objects inherit properties through the prototype chain.
Prototype Chain
Every object in JavaScript has a hidden link to another object — called its prototype. When you access a property on an object, JavaScript first looks on the object itself. If it does not find it there, it looks on the prototype. Then the prototype's prototype. And so on — until it either finds the property or reaches the end of the chain.
This chain of prototypes is called the prototype chain.
const user = { name: "Ali" };
console.log(user.name); // Ali — found on the object itself
console.log(user.toString()); // [object Object] — found on the prototypeuser does not have a toString method. But it works. JavaScript walked up the prototype chain and found it on Object.prototype.
Every Object Has a Prototype
When you create a plain object, JavaScript automatically links it to Object.prototype — an object that contains methods like toString(), hasOwnProperty(), valueOf().
const user = { name: "Ali" };
// Access the prototype
const proto = Object.getPrototypeOf(user);
console.log(proto === Object.prototype); // true
console.log(proto.toString); // function toString() { [native code] }Object.getPrototypeOf(obj) returns the prototype of any object — the modern way to access it.
The Prototype Chain
When you access a property:
- JavaScript looks on the object itself
- If not found — looks on the prototype
- If not found — looks on the prototype's prototype
- Continues until
nullis reached - If still not found — returns
undefined
const user = { name: "Ali" };
// Property lookup order
console.log(user.name); // 1. found on user itself — "Ali"
console.log(user.toString()); // 2. not on user → found on Object.prototype
console.log(user.nonExistent); // 3. not on user → not on Object.prototype → null → undefinedSetting Up a Prototype
Object.create() — create with a specific prototype
const animal = {
breathe() {
return `${this.name} is breathing.`;
},
eat(food) {
return `${this.name} eats ${food}.`;
}
};
// Create an object with animal as its prototype
const dog = Object.create(animal);
dog.name = "Rex";
dog.bark = function() {
return `${this.name} barks!`;
};
console.log(dog.bark()); // Rex barks! — own method
console.log(dog.breathe()); // Rex is breathing. — from prototype
console.log(dog.eat("meat")); // Rex eats meat. — from prototype
console.log(Object.getPrototypeOf(dog) === animal); // truedog does not have breathe or eat — but it inherits them from animal through the prototype chain.
How Prototype Chain Lookup Works — Step by Step
const vehicle = {
type: "vehicle",
describe() {
return `I am a ${this.type}`;
}
};
const car = Object.create(vehicle);
car.type = "car";
car.drive = function() {
return `${this.type} is driving`;
};
const tesla = Object.create(car);
tesla.brand = "Tesla";The chain looks like this:
console.log(tesla.brand); // Tesla — own property
console.log(tesla.type); // car — from car prototype
console.log(tesla.drive()); // car is driving — from car prototype
console.log(tesla.describe()); // I am a car — from vehicle prototype
// this.type is "car" because tesla inherits car's type
console.log(tesla.toString()); // [object Object] — from Object.prototypeNotice tesla.describe() returns "I am a car" — not "I am a vehicle". Because this always refers to the object the method was called on — tesla. And tesla inherits type: "car" from car.
Own Properties vs Inherited Properties
const dog = Object.create(animal);
dog.name = "Rex";
// Check own properties only
console.log(dog.hasOwnProperty("name")); // true — own
console.log(dog.hasOwnProperty("breathe")); // false — inherited
// Object.keys only returns own enumerable properties
console.log(Object.keys(dog)); // ["name"]
// for...in includes inherited enumerable properties
for (const key in dog) {
console.log(key); // name, breathe, eat
}
// Filter to own properties only in for...in
for (const key in dog) {
if (dog.hasOwnProperty(key)) {
console.log(key); // name only
}
}Constructor Functions and Prototypes
Before classes existed, JavaScript used constructor functions with new to create objects with shared prototypes.
function Person(name, age) {
this.name = name;
this.age = age;
}
// Methods go on the prototype — shared across all instances
Person.prototype.greet = function() {
return `Hello, I am ${this.name}.`;
};
Person.prototype.describe = function() {
return `${this.name} is ${this.age} years old.`;
};
const ali = new Person("Ali", 22);
const sara = new Person("Sara", 25);
console.log(ali.greet()); // Hello, I am Ali.
console.log(sara.greet()); // Hello, I am Sara.
console.log(ali.describe()); // Ali is 22 years old.
// Both share the same greet method — not copied per instance
console.log(ali.greet === sara.greet); // true — same function referenceWhen you use new:
- A new empty object is created
- Its prototype is set to
Person.prototype - The constructor function runs with
thispointing to the new object - The object is returned
This is exactly what ES6 classes do under the hood — they are syntactic sugar over this pattern.
Classes and Prototypes
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
const cat = new Animal("Whiskers");
// The class method lives on the prototype
console.log(cat.hasOwnProperty("name")); // true — set in constructor
console.log(cat.hasOwnProperty("speak")); // false — on prototype
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true
console.log(Animal.prototype.speak === cat.speak); // trueEvery instance of Animal shares the same speak method through the prototype — not a copy per instance. This is efficient and is the whole point of the prototype system.
Prototype Chain With Inheritance
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
return `${this.name} barks!`;
}
}
const rex = new Dog("Rex", "Labrador");console.log(rex.bark()); // Rex barks! — Dog.prototype
console.log(rex.speak()); // Rex makes a sound. — Animal.prototype
console.log(rex.toString()); // [object Object] — Object.prototype
// Checking the chain
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // trueModifying Prototypes
You can add methods to a prototype at any time — all existing instances immediately get access.
function Person(name) {
this.name = name;
}
const ali = new Person("Ali");
const sara = new Person("Sara");
// Add method after instances are created
Person.prototype.wave = function() {
return `${this.name} waves!`;
};
console.log(ali.wave()); // Ali waves! — immediately available
console.log(sara.wave()); // Sara waves! — immediately availableNever modify built-in prototypes
// ❌ Never do this — breaks other code and causes conflicts
Array.prototype.sum = function() {
return this.reduce((total, n) => total + n, 0);
};
String.prototype.shout = function() {
return this.toUpperCase() + "!!!";
};Modifying built-in prototypes like Array.prototype or String.prototype is dangerous — it pollutes the global prototype and can break third-party code or future JavaScript features that use the same name.
Never modify built-in prototypes in production code. It is one of the most dangerous things you can do in JavaScript. Use utility functions or subclasses instead.
Object.create(null) — No Prototype
Sometimes you want a truly empty object with no prototype at all — useful for pure dictionaries.
const dict = Object.create(null);
dict.name = "Ali";
dict.age = 22;
console.log(dict.name); // Ali
console.log(dict.toString); // undefined — no prototype at all
console.log(dict.hasOwnProperty); // undefined — no Object.prototype
// Safe to use as a dictionary — no inherited property name conflicts
const key = "hasOwnProperty";
dict[key] = "my own value"; // ✅ no conflict with Object.prototype.hasOwnPropertyKey Prototype Methods
const obj = { name: "Ali" };
// Get the prototype
Object.getPrototypeOf(obj); // Object.prototype
// Set the prototype (after creation — avoid in performance-critical code)
Object.setPrototypeOf(obj, myProto);
// Create with specific prototype
const child = Object.create(parentProto);
// Check if property is own
obj.hasOwnProperty("name"); // true
// Modern replacement for hasOwnProperty
Object.hasOwn(obj, "name"); // true
// Check prototype chain
obj instanceof Object; // trueA Real Example — Shared Utility Methods
// Base validator with shared methods
function Validator(value) {
this.value = value;
this.errors = [];
}
Validator.prototype.addError = function(message) {
this.errors.push(message);
return this;
};
Validator.prototype.isValid = function() {
return this.errors.length === 0;
};
Validator.prototype.getErrors = function() {
return [...this.errors];
};
// Specialized string validator
function StringValidator(value) {
Validator.call(this, value); // call parent constructor
}
StringValidator.prototype = Object.create(Validator.prototype);
StringValidator.prototype.constructor = StringValidator;
StringValidator.prototype.minLength = function(min) {
if (this.value.length < min) {
this.addError(`Must be at least ${min} characters.`);
}
return this;
};
StringValidator.prototype.maxLength = function(max) {
if (this.value.length > max) {
this.addError(`Must be no more than ${max} characters.`);
}
return this;
};
StringValidator.prototype.noSpaces = function() {
if (this.value.includes(" ")) {
this.addError("Cannot contain spaces.");
}
return this;
};
// Usage
const validator = new StringValidator("ali hassan");
validator
.minLength(3)
.maxLength(20)
.noSpaces();
console.log(validator.isValid()); // false
console.log(validator.getErrors()); // ["Cannot contain spaces."]
const valid = new StringValidator("ali_dev");
valid.minLength(3).maxLength(20).noSpaces();
console.log(valid.isValid()); // trueAll validators share addError, isValid, and getErrors through the prototype — not copied per instance. StringValidator inherits from Validator through the prototype chain — the same pattern ES6 classes use internally.
Summary
- Every JavaScript object has a hidden link to its prototype
- When a property is not found on an object, JavaScript walks up the prototype chain until it finds it or reaches
null Object.getPrototypeOf(obj)returns an object's prototypeObject.create(proto)creates a new object with a specific prototype- Class methods live on the prototype — shared across all instances, not copied per instance
instanceofchecks the entire prototype chain — not just the direct class- Own properties live on the object — inherited properties live on the prototype
- Use
hasOwnProperty()orObject.hasOwn()to check if a property is own - Constructor functions with
.prototypeare the old way to set up prototypes — ES6 classes are cleaner syntax for the same thing - Never modify built-in prototypes like
Array.prototypeorString.prototype