JavaScriptMedium

How does prototypal inheritance work?

01

The Short Answer

Prototypal inheritance is JavaScript's mechanism for sharing properties and methods between objects. Instead of classes defining blueprints, objects inherit directly from other objects through an internal link called the prototype chain. When you access a property on an object, JavaScript looks up the chain until it finds it or reaches the end (null).

02

The Prototype Chain

Every object in JavaScript has a hidden internal property called [[Prototype]] (accessible via __proto__ or Object.getPrototypeOf()). This property points to another object — its prototype. When you try to access a property that doesn't exist on the object itself, JavaScript follows this link to the prototype, then to the prototype's prototype, and so on until it either finds the property or hits null.

The following example creates a simple prototype chain manually using Object.create(). Notice how dog doesn't have its own speak method, but can still call it because JavaScript walks up the chain to animal where the method lives.

prototype-chain.tstypescript
const animal = {
  isAlive: true,
  speak() {
    console.log(`${this.name} makes a sound`);
  },
};

// dog's prototype is animal
const dog = Object.create(animal);
dog.name = 'Rex';
dog.fetch = function () {
  console.log(`${this.name} fetches the ball`);
};

dog.fetch();   // "Rex fetches the ball" — own property
dog.speak();   // "Rex makes a sound" — found on prototype (animal)
dog.isAlive;   // true — also from prototype
dog.toString(); // "[object Object]" — from Object.prototype (top of chain)

// The chain: dog → animal → Object.prototype → null

The lookup is: check dog itself → not found → check animal (dog's prototype) → found. This is the prototype chain in action. If it wasn't on animal either, JavaScript would continue to Object.prototype, and finally return undefined if it reaches null.

03

Constructor Functions and .prototype

Before ES6 classes, constructor functions were the standard way to create objects with shared behavior. Every function has a .prototype property — an object that becomes the [[Prototype]] of instances created with new. Methods placed on Constructor.prototype are shared across all instances without being duplicated in memory.

constructor-prototype.tstypescript
function User(name: string, email: string) {
  this.name = name;
  this.email = email;
}

// Methods on the prototype — shared by ALL instances
User.prototype.greet = function () {
  return `Hi, I'm ${this.name}`;
};

User.prototype.getEmail = function () {
  return this.email;
};

const alice = new User('Alice', 'alice@example.com');
const bob = new User('Bob', 'bob@example.com');

alice.greet(); // "Hi, I'm Alice"
bob.greet();   // "Hi, I'm Bob"

// Both share the SAME function object
alice.greet === bob.greet; // true — not duplicated

// The chain: alice → User.prototype → Object.prototype → null

Key distinction

User.prototype is NOT the prototype of User itself. It's the prototype that gets assigned to objects created with new User(). The prototype of the User function itself is Function.prototype.

04

How ES6 Classes Use Prototypes

ES6 classes are syntactic sugar over prototypal inheritance — they don't introduce a new inheritance model. Under the hood, a class declaration creates a constructor function and puts methods on its .prototype. The extends keyword sets up the prototype chain between the child and parent prototypes.

class-sugar.tstypescript
class Animal {
  constructor(public name: string) {}

  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  fetch() {
    return `${this.name} fetches the ball`;
  }
}

const rex = new Dog('Rex');
rex.fetch(); // "Rex fetches the ball" — on Dog.prototype
rex.speak(); // "Rex makes a sound" — on Animal.prototype

// Under the hood, the chain is:
// rex → Dog.prototype → Animal.prototype → Object.prototype → null

// Proof it's still prototypal:
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true

The class syntax makes the code cleaner, but the mechanism is identical. Methods live on prototype objects, and extends links them together. There's no copying of methods — it's all delegation through the prototype chain.

05

Own Properties vs Inherited Properties

JavaScript distinguishes between properties that live directly on an object (own properties) and those found via the prototype chain (inherited). This distinction matters when iterating over properties or checking if an object has a specific property.

own-vs-inherited.tstypescript
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;

// hasOwnProperty — checks ONLY the object itself
child.hasOwnProperty('own');       // true
child.hasOwnProperty('inherited'); // false

// 'in' operator — checks the entire prototype chain
'own' in child;       // true
'inherited' in child; // true

// Object.keys — returns only own enumerable properties
Object.keys(child); // ['own']

// for...in — iterates own AND inherited enumerable properties
for (const key in child) {
  console.log(key); // 'own', then 'inherited'
}
06

Property Shadowing

When you set a property on an object that already exists on its prototype, the new property is created on the object itself — it doesn't modify the prototype. The own property "shadows" the inherited one. This is how instances can have their own values while sharing default behavior from the prototype.

shadowing.tstypescript
const defaults = { theme: 'light', language: 'en' };
const userPrefs = Object.create(defaults);

userPrefs.theme; // 'light' — inherited from defaults

// Setting a property creates it on userPrefs, doesn't touch defaults
userPrefs.theme = 'dark';

userPrefs.theme;  // 'dark' — own property (shadows the inherited one)
defaults.theme;   // 'light' — unchanged

// Deleting the own property reveals the inherited one again
delete userPrefs.theme;
userPrefs.theme; // 'light' — back to inherited
07

Common Mistakes

🔗

Mutating shared reference types on the prototype

If a prototype has an array or object property, all instances share the same reference. Pushing to `instance.items` modifies the prototype's array — affecting every instance.

Initialize reference types (arrays, objects) in the constructor so each instance gets its own copy. Only put methods and primitive defaults on the prototype.

🔄

Replacing .prototype after creating instances

If you reassign `Constructor.prototype` to a new object after instances already exist, those instances still point to the old prototype. New instances get the new one — creating inconsistent behavior.

Set up the prototype before creating any instances, or use `Object.setPrototypeOf()` (though this has performance implications).

08

Why Interviewers Ask This

Prototypal inheritance is the foundation of JavaScript's object system. Interviewers ask this to check whether you understand how property lookup works, the relationship between constructors and prototypes, why ES6 classes are sugar (not a new model), and how shared methods save memory. It reveals whether you understand JavaScript at a fundamental level or just use class syntax without knowing what's happening underneath.

Quick Revision Cheat Sheet

Prototype chain: Objects inherit from other objects via [[Prototype]] links

Property lookup: Check own → prototype → prototype's prototype → ... → null

Constructor.prototype: Object that becomes [[Prototype]] of instances created with `new`

ES6 classes: Syntactic sugar — still uses prototypal inheritance underneath

Shadowing: Setting a property on an object hides the inherited one without modifying the prototype