Skip to content

Module 2: Prototypical Inheritance Mechanics

Introduction to JavaScript's Inheritance Model

Welcome to the second module of our journey into Modern JavaScript architecture. In traditional object-oriented languages like Java or C++, inheritance flows downward through rigid blueprints known as classes. A child class inherits the skeletal structure of a parent class before an object is ever instantiated in memory. Over the decades, many developers migrating to JavaScript attempted to force this exact mental model onto the language, leading to immense confusion and brittle codebases.

JavaScript is radically different. Classical software engineering blueprints do not exist under the hood. Instead, JavaScript handles inheritance dynamically, at runtime, through a powerful delegation mechanism known as Prototypical Inheritance. At its core, Prototypical Inheritance is simply live objects linking to other live objects. If an object cannot find a property or method on itself, it doesn't give up; it delegates the lookup request to its "parent" object—its prototype. Mastering this chain of delegation is the absolute key to writing highly performant, memory-efficient JavaScript. In this chapter, we will dissect the prototype chain, analyze how property resolution works mechanically in the engine, and learn how to forge multi-level inheritance structures manually.

The Problem: Memory Bloat in Constructors

In the previous chapter, we left off with Constructor functions. They standardized object creation but left us with a silent, critical scalability flaw. Let's look at it closely.

// A standard Constructor Function
function Animal(name, species) {
  this.name = name;
  this.species = species;

  // A method attached directly to 'this'
  this.makeSound = function() {
    console.log(`${this.name} makes a noise.`);
  };
}

const dog = new Animal('Buddy', 'Dog');
const cat = new Animal('Whiskers', 'Cat');

What exactly is the problem here?

When the new keyword executes, it binds this to a fresh, empty object. This means for every single Animal we create, the engine allocates physical memory space in the Heap for a distinct makeSound function. If you create 1,000,000 Animal objects for a large-scale game, you are creating 1,000,000 identical copies of the makeSound function! This memory bloat will trigger aggressive Garbage Collection, freezing the browser thread and ruining user performance.

We need a way for all 1,000,000 instances to share a single makeSound method in memory.

The Solution: The .prototype Property

To solve this memory crisis, we turn to the .prototype property. Every function in JavaScript automatically comes with a property named prototype. This property is simply an object. It acts as a central repository for shared methods and state.

Let's refactor our Animal constructor.

Code Walkthrough: Sharing Methods via Prototype

// Line 1: We define our constructor, focusing purely on UNIQUE state.
function Animal(name, species) {
  // Line 3: Name and species denote unique data, so they stay on 'this'.
  this.name = name;
  this.species = species;
}

// Line 8: We attach the shared method onto the constructor's prototype.
Animal.prototype.makeSound = function() {
  console.log(`${this.name} makes a noise.`);
};

// Line 13: We instantiate the objects.
const lion = new Animal('Simba', 'Lion');
const tiger = new Animal('Rajah', 'Tiger');

// Line 17: We call the method.
lion.makeSound(); // Prints: Simba makes a noise.

Step-by-Step Breakdown:

  1. We stripped the makeSound function out of the constructor body. Now, when we instantiate lion and tiger, they only hold the primitive strings name and species in their memory space.
  2. We defined makeSound exactly once in memory, placing it inside the Animal.prototype object.
  3. When we call lion.makeSound(), the JavaScript Engine checks the lion object. "Do you have a property named makeSound?" The answer is NO.
  4. Instead of throwing a "TypeError: makeSound is not a function", the engine implicitly traverses the secret link to Animal.prototype. It finds makeSound there and executes it.

Because lion and tiger share the exact same prototype reference, our memory footprint remains minuscule, no matter how many millions of animals we instantiate.

Pro-Tip: Defining State vs. Behavior As a best practice, you should place structural properties (state) inside the Constructor function using this, ensuring every instance gets its own independent copy of the data. Conversely, you should place methods (behavior) on the .prototype object, ensuring optimal memory efficiency.


Dissecting the Prototype Chain: __proto__ vs .prototype

There is immense confusion regarding two extremely similar sounding terms: __proto__ and .prototype. Let's lay this debate to rest theoretically and mechanically.

1. The Internal Slot: [[Prototype]] / __proto__

Every single object in JavaScript (whether created literally, by a factory, or a constructor) has a hidden internal slot defined in the ECMAScript specification as [[Prototype]]. Browsers exposed this slot via the __proto__ getter/setter property.

The __proto__ property is the actual, tangible pipeline that points from a child object up to its parent object. It is the literal chain link.

2. The Blueprint Property: .prototype

The .prototype property only exists natively on Functions (specifically, normal and constructor functions, not arrow functions). It has zero effect on the function itself. Its entire purpose is to provide the blueprint for the __proto__ linkage when the function is invoked with the new keyword.

The Mechanical Rule

When you execute const obj = new Constr(), the engine automatically does the following behind the scenes: obj.__proto__ = Constr.prototype;

Concept What is it? Where does it live? Purpose
__proto__ An object reference (pointer). On every single object. It traces the chain backwards to find inherited properties during runtime lookups.
.prototype A normal object. Only on Functions. It acts as the staging area. It is the object that __proto__ will point to upon instantiation.

Forging Multi-Level Inheritance

In a professional architecture, a single level of inheritance is rarely enough. What if we want a generic Vehicle, a specific Car that inherits from Vehicle, and an even more specific SportsCar that inherits from Car?

We must manually weave the prototype chain using the Object.create() method.

Code Walkthrough: Weaving the Chain

/* --- LEVEL 1: The Base Parent --- */
function Vehicle(engineType) {
  this.engineType = engineType;
}
Vehicle.prototype.startEngine = function() {
  console.log(`Starting ${this.engineType} engine...`);
};

/* --- LEVEL 2: The Child --- */
function Car(engineType, brand) {
  // Line 11: We execute the Vehicle constructor inside this context, 
  // ensuring the 'this' context inherits the base state properties.
  Vehicle.call(this, engineType);
  this.brand = brand;
}

// Line 17: We link the prototypes! Object.create() returns a brand new 
// empty object whose internal __proto__ points to Vehicle.prototype.
Car.prototype = Object.create(Vehicle.prototype);

// Line 21: Since we replaced Car.prototype entirely, we lost the 
// original constructor reference. We must restore it manually.
Car.prototype.constructor = Car;

Car.prototype.drive = function() {
  console.log(`Driving a ${this.brand} car.`);
};

/* --- EXECUTION --- */
const myCorolla = new Car('V4', 'Toyota');
myCorolla.startEngine(); // Output: Starting V4 engine...
myCorolla.drive();       // Output: Driving a Toyota car.

Step-by-Step Breakdown of Resolution: Look closely at myCorolla.startEngine(). Here is the exact path the Engine takes:

  1. Engine Check 1: Does the myCorolla object have startEngine? No. It only has brand and engineType.
  2. Link Traversal: The Engine climbs myCorolla.__proto__, which leads to Car.prototype.
  3. Engine Check 2: Does Car.prototype have startEngine? No. It only has drive and constructor.
  4. Link Traversal: The Engine climbs Car.prototype.__proto__, which leads to Vehicle.prototype.
  5. Engine Check 3: Does Vehicle.prototype have startEngine? YES! The execution occurs, and the search immediately terminates.

If the search reached Object.prototype (the absolute top of the chain) and still found nothing, it would finally return undefined.

Common Pitfall: Method Overriding If you defined Car.prototype.startEngine = function() { ... }, the lookup path would stop at step 3. Because it found a match on Car.prototype, the engine abandons the search, meaning the parent's startEngine method is permanently "shadowed." This is known as Method Overriding, an incredibly common and useful OOP technique.


🧠 Knowledge Check: Self-Assessment

Can you trace the prototype chain perfectly? Read the code below and determine exactly what the output will be based on the mechanical lookup rules we just covered.

function Employee(name) {
  this.name = name;
}

Employee.prototype.getRole = function() {
  return "General Employee";
};

function Manager(name) {
  Employee.call(this, name);
}

// Setting up the chain
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;

// Overriding the method
Manager.prototype.getRole = function() {
  return "Regional Manager";
};

const boss = new Manager("Sarmad");

console.log(boss.name);
console.log(boss.getRole());

// The Trick Question execution
delete boss.getRole;
console.log(boss.getRole());

Analyze the output:

Consider the following:

  • Which getRole() function fires first?
  • What exactly does the delete operator target? Does it have the power to jump up the prototype chain?
View the Comprehensive Answer and Breakdown: ### Output Summary
Sarmad
Regional Manager
Regional Manager
### Step-by-Step Breakdown 1. **`console.log(boss.name);`** The constructor `Employee.call(this, name)` ensured that the `name` property was created directly on the instance inside the Heap memory. Therefore, `boss` owns `name`, and this prints `Sarmad`. 2. **`console.log(boss.getRole());`** The engine checks the `boss` object. It finds no method. It traverses `__proto__` up to `Manager.prototype`. It finds `getRole` returning `"Regional Manager"`. The engine terminates the search and executes it. 3. **`delete boss.getRole;` followed by `console.log(boss.getRole());`** This is the trap. The `delete` keyword in JavaScript behaves very rigidly: it *only* attempts to delete a property if it exists strictly on the specified physical object level. It looks at the `boss` instance itself in the Heap memory and attempts to delete `getRole`. However, `boss` **does not have** a `getRole` property directly on it—the method is living identically on `Manager.prototype`. Because `delete` cannot reach up the prototype chain to destroy inherited methods, the statement silently fails (or strictly does nothing). When we call `boss.getRole()` the second time, the engine performs the exact same traversal path as before, hitting `Manager.prototype` and printing `"Regional Manager"` again! If you wanted to actually destroy the method for all managers, you would have to target the prototype explicitly: `delete Manager.prototype.getRole;`. Which then would cause `boss.getRole()` to traverse even higher, hitting `Employee.prototype` and printing `"General Employee"`!

In our next module, we will discover how ES6 finally provided us with standard, beautiful syntactical sugar to avoid writing Object.create ever again.