Prototypes & the Prototype Chain

intermediate prototype prototype-chain __proto__ Object.create inheritance

JavaScript doesn’t have classical inheritance like Java or C++. Instead, it uses something called prototypal inheritance — objects inherit directly from other objects. This is one of the most important concepts in JS, and once it clicks, a lot of confusing behavior suddenly makes sense.

Every object has a prototype

Every object in JavaScript has a hidden internal link called [[Prototype]]. It points to another object — the object’s prototype. When we try to access a property that doesn’t exist on the object, JavaScript follows this link and looks for the property on the prototype. If it’s not there either, it goes to the prototype’s prototype, and so on. This chain of lookups is the prototype chain.

myObj
{ name: "Manish" }
[[Prototype]]
Person.prototype
{ greet(), constructor }
[[Prototype]]
Object.prototype
{ toString(), hasOwnProperty(), ... }
[[Prototype]]
null
end of chain

__proto__ vs .prototype

This is where most people get confused. Let’s clear it up:

  • __proto__ (or [[Prototype]]) exists on every object. It’s the actual link to the object’s prototype. We should use Object.getPrototypeOf(obj) instead of __proto__ directly (it’s deprecated but still works).
  • .prototype exists only on functions. It’s the object that will become the [[Prototype]] of instances created with new.
function Person(name) {
  this.name = name;
}

const manish = new Person("Manish");

// manish.__proto__ === Person.prototype  (true)
// Person.prototype is the blueprint for instances
console.log(Object.getPrototypeOf(manish) === Person.prototype); // true

Think of it like this: Person.prototype is a stencil. When we use new Person(), the new object’s __proto__ gets pointed at that stencil.

How the prototype chain lookup works

When we access a property on an object, JavaScript does the following:

  1. Check the object itself — is the property there? If yes, use it.
  2. If not, check object.__proto__ (i.e., the prototype).
  3. If still not found, check object.__proto__.__proto__.
  4. Keep going until we hit null (end of the chain).
  5. If the property isn’t found anywhere, return undefined.
function Animal(type) {
  this.type = type;
}
Animal.prototype.speak = function () {
  return `${this.type} makes a sound`;
};

const dog = new Animal("Dog");

console.log(dog.type);     // "Dog" — found on dog itself
console.log(dog.speak());  // "Dog makes a sound" — found on Animal.prototype
console.log(dog.toString()); // "[object Object]" — found on Object.prototype

Object.create()

Object.create() creates a new object with a specific prototype. It’s the cleanest way to set up prototype-based inheritance without constructor functions:

const parent = {
  greet() {
    return `Hello, I'm ${this.name}`;
  }
};

const child = Object.create(parent); // child's [[Prototype]] is parent
child.name = "Manish";
console.log(child.greet()); // "Hello, I'm Manish"

We can also create an object with no prototype at all:

const bare = Object.create(null);
console.log(bare.toString); // undefined — no Object.prototype methods!

Property shadowing

When we set a property on an object that already exists on its prototype, the new property shadows (hides) the prototype’s version. The prototype property is still there — it’s just not reachable through this object anymore.

function Car(brand) {
  this.brand = brand;
}
Car.prototype.honk = function () { return "Beep!"; };

const myCar = new Car("Toyota");
console.log(myCar.honk()); // "Beep!" — from prototype

myCar.honk = function () { return "HONK!"; }; // shadows the prototype method
console.log(myCar.honk()); // "HONK!" — own property now

delete myCar.honk; // remove the shadow
console.log(myCar.honk()); // "Beep!" — prototype method is back

hasOwnProperty vs in

  • in checks the entire prototype chain
  • hasOwnProperty() (or the modern Object.hasOwn()) checks only the object’s own properties
const obj = Object.create({ inherited: true });
obj.own = true;

console.log("own" in obj);       // true
console.log("inherited" in obj); // true

console.log(obj.hasOwnProperty("own"));       // true
console.log(obj.hasOwnProperty("inherited")); // false

End of the chain

The prototype chain always ends at Object.prototype, whose own [[Prototype]] is null:

console.log(Object.getPrototypeOf(Object.prototype)); // null

When JavaScript reaches null, the lookup stops. If the property wasn’t found anywhere in the chain, we get undefined.

In simple language, the prototype chain is how JavaScript shares behavior between objects. When we look up a property, JS walks up the chain of linked objects until it finds what it’s looking for (or hits null). Every object is linked to a prototype, and that’s how methods like toString() magically work on every object even though we never defined them.