Prototype Chain: The Core of JavaScript Inheritance

Posted by

A deep dive into how objects link together in JavaScript, how __proto__ and prototype really work, and why understanding the prototype chain saves you from bugs and design mistakes.

A deep dive into how objects link together in JavaScript, how __proto__ and prototype really work, and why understanding the prototype chain saves you from bugs and design mistakes.

Introduction

JavaScript doesn’t have “classes” in the same sense as Java or C++. Under the hood, it’s all about objects linked to other objects. This linking is called the prototype chain, and it’s the backbone of inheritance in JS.

Every object in JavaScript has a hidden property ([[Prototype]]) that points to another object. If you try to access a property that doesn’t exist on the current object, JS walks up the chain until it finds it—or hits the end (null).

Mastering this concept means:

  • You’ll debug weird “why is this property available?” bugs.
  • You’ll write efficient object models (before and beyond class).
  • You’ll grok why Object.create, new, and class all work the way they do.

Let’s unpack it step by step with real code.


1. Objects and Their Hidden Link

Every JS object has an internal slot called [[Prototype]]. You can peek at it with Object.getPrototypeOf(obj) or obj.__proto__ (the non-standard accessor).

const animal = { eats: true };
const dog = { barks: true };

Object.setPrototypeOf(dog, animal);

console.log(dog.barks); // true (own property)
console.log(dog.eats); // true (inherited from animal)

If dog doesn’t have eats, JS looks up its prototype: animal.


2. Walking the Prototype Chain

console.log(dog.toString);

Where does toString come from?

  • Not on dog.
  • Not on animal.
  • Found on Object.prototype.

At the top:

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

👉 That’s the end of the chain.


3. Functions and the prototype Property

Functions in JS are special objects: they have a prototype property that’s used when you call them with new.

function Animal(name) {
this.name = name;
}
Animal.prototype.eats = true;

const cat = new Animal("Kitty");

console.log(cat.name); // "Kitty" (own property)
console.log(cat.eats); // true (inherited from Animal.prototype)

Mechanics:

  1. new Animal("Kitty") creates a blank object.
  2. Sets its [[Prototype]] to Animal.prototype.
  3. Runs Animal with this bound to the new object.

4. Prototype Chain in Classes (Sugar Syntax)

ES6 class is just syntactic sugar around the same mechanism.

class Animal {
constructor(name) { this.name = name; }
speak() { console.log(this.name + " makes a sound"); }
}

class Dog extends Animal {
speak() { console.log(this.name + " barks"); }
}

const rex = new Dog("Rex");
rex.speak(); // "Rex barks"

Behind the scenes:

  • Dog.prototype.__proto__ === Animal.prototype.
  • Animal.prototype.__proto__ === Object.prototype.

So rex can access methods from Dog, then Animal, then Object.


5. Visualizing the Chain

rex
↑ [[Prototype]]
Dog.prototype
↑ [[Prototype]]
Animal.prototype
↑ [[Prototype]]
Object.prototype
↑ [[Prototype]]
null

6. Shadowing and Property Lookup

If an object has its own property with the same name as an inherited one, it shadows it.

const base = { greet: () => "hi" };
const child = Object.create(base);

child.greet = () => "hello";
console.log(child.greet()); // "hello" (own shadows prototype)

delete child.greet;
console.log(child.greet()); // "hi" (falls back to prototype)

7. Real-World Examples

Custom Methods via Prototype

Array.prototype.first = function() {
return this[0];
};

console.log([10, 20, 30].first()); // 10

(Extending built-ins is risky — can break polyfills or libraries.)

Polyfills

if (!String.prototype.includes) {
String.prototype.includes = function(sub) {
return this.indexOf(sub) !== -1;
};
}

React/Node Class Hierarchies

React components: class MyComponent extends React.Component {}.
Node’s EventEmitter: many core modules inherit from it.


8. Object.create — Pure Prototype Magic

const animal = { eats: true };
const rabbit = Object.create(animal);

console.log(rabbit.eats); // true (from prototype)
console.log(Object.getPrototypeOf(rabbit) === animal); // true

Great for lightweight inheritance without constructors.


9. Performance Considerations

  • Prototype lookups are fast (optimized in modern engines).
  • But deep chains make debugging harder.
  • Avoid modifying prototypes of built-ins in libraries — can cause collisions.

10. Common Gotchas

Arrow Functions on Prototypes

function Foo() {}
Foo.prototype.bar = () => this;
console.log(new Foo().bar()); // ❌ not Foo instance, it's outer `this`

Use normal functions on prototypes.

Misunderstanding __proto__ vs prototype

  • obj.__proto__ → the prototype of an object (its hidden link).
  • Func.prototype → the object used as the prototype for new instances created with new Func().

Static Methods

class A {
static greet() { return "hi"; }
}
console.log(A.greet()); // "hi"
console.log(new A().greet()); // ❌ TypeError

Static methods live on the constructor function itself, not on .prototype.


11. Quick Reference Table


Conclusion

The prototype chain is the engine of JavaScript’s inheritance. Instead of copying methods into each object, JS links them through [[Prototype]]. This means:

  • Objects share behavior efficiently.
  • Classes and extends are just sugar for prototype links.
  • You can inspect and manipulate prototypes directly with Object.create, Object.getPrototypeOf, and Object.setPrototypeOf.

Pro tip: When debugging property lookups, ask: Does this belong to the object itself, or is it coming from somewhere higher in the chain? That question alone saves hours of confusion.


Call to Action

What’s the weirdest prototype bug you’ve hit — shadowed props, lost this, or __proto__ confusion?

💬 Share in the comments.
🔖 Bookmark this as your prototype cheat sheet.
👩‍💻 Send to a teammate who still thinks class is “real inheritance” in JS.

Leave a Reply

Your email address will not be published. Required fields are marked *