DEV Community

Samuel Ochaba
Samuel Ochaba

Posted on

Understanding Prototypal Inheritance in JavaScript: A Deep Dive

Prototypal inheritance is one of JavaScript's most powerful yet misunderstood features. Unlike classical inheritance found in languages like Java or C++, JavaScript uses a prototype-based approach that's both flexible and elegant. Let's unpack this concept from the ground up.

The Problem: Code Reusability

Imagine you're building a user management system. You have a basic user object with common properties and methods. Now you need to create admin and guest variants. Should you copy-paste all the user code? Absolutely not! This is where prototypal inheritance shines—it lets you build new objects on top of existing ones.

The Hidden [[Prototype]] Property

Every JavaScript object has a secret weapon: a hidden property called [[Prototype]]. Think of it as a reference pointer that says, "If you can't find what you're looking for in me, check this other object."

This [[Prototype]] can point to:

  • Another object (your prototype)
  • null (end of the chain)

When you try to access a property or method that doesn't exist on an object, JavaScript automatically searches up the prototype chain until it finds it—or reaches null.

Setting Up Prototypes with __proto__

The historical way to access [[Prototype]] is through __proto__. Here's a classic example:

let animal = {
  eats: true,
  walk() {
    console.log("Animal walks");
  }
};

let rabbit = {
  jumps: true
};

// Set animal as rabbit's prototype
rabbit.__proto__ = animal;

console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats);  // true (inherited from animal)
rabbit.walk();             // "Animal walks" (inherited method)
Enter fullscreen mode Exit fullscreen mode

When we access rabbit.eats, JavaScript thinks: "I don't see eats in rabbit... let me check its prototype. Ah! Found it in animal!"

Important: __proto__ vs [[Prototype]]

Here's a crucial distinction that trips up many developers:

  • [[Prototype]] is the actual internal property
  • __proto__ is a getter/setter for accessing it

Modern JavaScript recommends using Object.getPrototypeOf() and Object.setPrototypeOf() instead, but __proto__ remains widely supported and intuitively clear for learning.

Prototype Chains: Going Deeper

Prototypes can form chains of arbitrary length:

let animal = {
  eats: true,
  walk() {
    console.log("Animal walks");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
};

longEar.walk();           // "Animal walks" (found in animal)
console.log(longEar.jumps); // true (found in rabbit)
console.log(longEar.eats);  // true (found in animal)
Enter fullscreen mode Exit fullscreen mode

JavaScript searches: longEarrabbitanimalObject.prototypenull

Prototype Chain Limitations

  1. No circular references: You can't create infinite loops
  2. Only objects or null: Primitives like strings or numbers are ignored
  3. Single inheritance: An object can't inherit from multiple prototypes directly

Writing vs Reading: A Critical Difference

Here's where things get interesting. Prototypes are read-only lookup chains. When you write to a property, it goes directly to the object itself:

let animal = {
  eats: true,
  walk() {
    console.log("Generic animal walk");
  }
};

let rabbit = {
  __proto__: animal
};

// This creates a new method on rabbit, doesn't modify animal
rabbit.walk = function() {
  console.log("Rabbit bounce!");
};

rabbit.walk(); // "Rabbit bounce!" (own method)
animal.walk(); // "Generic animal walk" (unchanged)
Enter fullscreen mode Exit fullscreen mode

The Accessor Property Exception

Accessor properties (getters/setters) are the exception. When you assign to an accessor property, you're actually calling its setter function:

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

console.log(admin.fullName);  // "John Smith" (getter from prototype)
admin.fullName = "Alice Cooper"; // Calls setter from prototype
console.log(admin.fullName);  // "Alice Cooper"
Enter fullscreen mode Exit fullscreen mode

The Magic of this: Always Points to the Caller

This is perhaps the most important concept: this is determined by the object before the dot, not where the method is defined.

let animal = {
  walk() {
    if (!this.isSleeping) {
      console.log(`${this.name} walks`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

let cat = {
  name: "Fluffy",
  __proto__: animal
};

rabbit.sleep(); // Sets rabbit.isSleeping = true
cat.sleep();    // Sets cat.isSleeping = true

console.log(rabbit.isSleeping); // true
console.log(cat.isSleeping);    // true
console.log(animal.isSleeping); // undefined
Enter fullscreen mode Exit fullscreen mode

Even though sleep() is defined in animal, when called on rabbit or cat, this refers to the calling object. This means methods are shared, but state is not—a beautiful design pattern!

Looping and Enumeration: Own vs Inherited

The for...in loop iterates over both own and inherited properties:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// Only own properties
console.log(Object.keys(rabbit)); // ["jumps"]

// Both own and inherited
for (let prop in rabbit) {
  console.log(prop); // "jumps", then "eats"
}
Enter fullscreen mode Exit fullscreen mode

To distinguish between own and inherited properties:

for (let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    console.log(`Own: ${prop}`);
  } else {
    console.log(`Inherited: ${prop}`);
  }
}
// Own: jumps
// Inherited: eats
Enter fullscreen mode Exit fullscreen mode

Why Doesn't hasOwnProperty Show Up in the Loop?

Excellent question! hasOwnProperty itself is inherited from Object.prototype, but it doesn't appear in for...in loops because it's marked as non-enumerable (enumerable: false). This is true for all Object.prototype methods.

Key Takeaways

  1. Prototypal inheritance enables code reuse without copying
  2. [[Prototype]] is the internal link, __proto__ is how we access it
  3. Reading follows the chain, writing happens directly on the object
  4. this always refers to the calling object, not where the method is defined
  5. Methods are shared, state is not—perfect for efficient object creation
  6. Most methods like Object.keys() ignore inherited properties, only for...in includes them

Practical Applications

Understanding prototypes is crucial for:

  • Working with JavaScript classes (which use prototypes under the hood)
  • Understanding how built-in objects like Arrays and Dates work
  • Optimizing memory usage by sharing methods across instances
  • Debugging unexpected behavior in object hierarchies
  • Leveraging advanced patterns like mixins and delegation

Prototypal inheritance might seem strange at first, especially if you come from a classical OOP background. But once it clicks, you'll appreciate its simplicity and power. It's the foundation of JavaScript's object model and understanding it deeply will make you a much more effective JavaScript developer.

Top comments (0)