DocsHub
Advanced

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 prototype

user 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

__proto__ __proto__ user objectname: Ali Object.prototypetoStringhasOwnPropertyvalueOf nullend of chain

When you access a property:

  1. JavaScript looks on the object itself
  2. If not found — looks on the prototype
  3. If not found — looks on the prototype's prototype
  4. Continues until null is reached
  5. 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 → undefined

Setting 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); // true

dog 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:

prototype prototype prototype prototype teslabrand: Tesla cartype: cardrive vehicletype: vehicledescribe Object.prototypetoString... null
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.prototype

Notice 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 reference

When you use new:

  1. A new empty object is created
  2. Its prototype is set to Person.prototype
  3. The constructor function runs with this pointing to the new object
  4. 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);            // true
prototype prototype prototype cat instancename: Whiskers Animal.prototypespeak method Object.prototypetoString... null

Every 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");
prototype prototype prototype prototype rex instancename: Rexbreed: Labrador Dog.prototypebark method Animal.prototypespeak method Object.prototypetoString... null
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); // true

Modifying 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 available

Never 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.hasOwnProperty

Key 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;                  // true

A 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()); // true

All 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 prototype
  • Object.create(proto) creates a new object with a specific prototype
  • Class methods live on the prototype — shared across all instances, not copied per instance
  • instanceof checks the entire prototype chain — not just the direct class
  • Own properties live on the object — inherited properties live on the prototype
  • Use hasOwnProperty() or Object.hasOwn() to check if a property is own
  • Constructor functions with .prototype are the old way to set up prototypes — ES6 classes are cleaner syntax for the same thing
  • Never modify built-in prototypes like Array.prototype or String.prototype

On this page