Home JavaScript JavaScript Prototypes and Inheritance Explained — How Objects Really Share Behaviour

JavaScript Prototypes and Inheritance Explained — How Objects Really Share Behaviour

In Plain English 🔥
Imagine every employee at a company gets a copy of the employee handbook on their first day. Instead of printing a fresh handbook for every single person, the company pins one master copy to the noticeboard and says 'if you need a rule, check there first.' That noticeboard is the prototype. Your object looks up what it needs, and if it doesn't have the answer itself, it walks up the chain until it finds it — or runs out of noticeboards to check.
⚡ Quick Answer
Imagine every employee at a company gets a copy of the employee handbook on their first day. Instead of printing a fresh handbook for every single person, the company pins one master copy to the noticeboard and says 'if you need a rule, check there first.' That noticeboard is the prototype. Your object looks up what it needs, and if it doesn't have the answer itself, it walks up the chain until it finds it — or runs out of noticeboards to check.

JavaScript is one of the few mainstream languages where inheritance isn't bolted on through classes in the traditional sense — it's baked into the very fabric of how objects work. Every single object you create, from a plain {} to a complex class instance, is silently connected to a chain of other objects. Understanding that chain isn't just academic trivia — it's the reason Array methods like .map() and .filter() exist on every array you've ever written without you defining them yourself.

Before ES6 classes arrived, the only way to share behaviour between objects was through prototypes directly. Classes are still prototypes underneath — they're syntactic sugar, not a different system. If you don't understand the prototype chain, you'll hit confusing bugs when properties seem to appear from nowhere, or when methods you thought were isolated suddenly affect every object in your app.

By the end of this article you'll be able to explain exactly what __proto__ and prototype mean and why they're different, build an inheritance hierarchy two ways (prototype-based and class-based) and know the trade-offs, spot and fix the most common prototype bugs, and answer the prototype questions that trip people up in senior JavaScript interviews.

What the Prototype Chain Actually Is — And Why JS Needs It

Every JavaScript object has an internal slot called [[Prototype]]. Think of it as a hidden pointer that says 'if you can't find a property on me, go look over there.' That 'over there' is another object — its prototype — which has its own [[Prototype]], and so on, until you reach Object.prototype, whose [[Prototype]] is null. That's the end of the chain.

This design solves a memory problem. If you create 10,000 User objects and each one stored its own copy of the greet() method, you'd have 10,000 identical functions in memory. With the prototype chain, you define greet() once on User.prototype and all 10,000 instances share a single reference to it. The method lives in one place; the lookup walks there automatically.

The browser exposes this via __proto__ (the instance's pointer to its prototype) and .prototype (a property on constructor functions that becomes the [[Prototype]] of objects they create). These are different things and mixing them up is the source of enormous confusion — we'll untangle that in the code below.

Property lookup always starts on the object itself. Only if the property isn't found there does JS walk up the chain. This means instance properties always shadow prototype properties — a critical detail when debugging unexpected values.

prototype-chain-basics.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132
// --- Demonstrating the prototype chain from scratch ---

// A plain object — no constructor, no class
const vehicleBlueprint = {
  describe() {
    // 'this' refers to whatever object called the method
    return `I am a ${this.type} that travels at ${this.topSpeed} km/h`;
  }
};

// Create a new object whose prototype IS vehicleBlueprint
const bicycle = Object.create(vehicleBlueprint);
bicycle.type = 'bicycle';       // own property — lives directly on bicycle
bicycle.topSpeed = 30;          // own property

const sportsCar = Object.create(vehicleBlueprint);
sportsCar.type = 'sports car';
sportsCar.topSpeed = 300;

// Both objects share ONE describe() function — it lives on vehicleBlueprint
console.log(bicycle.describe());    // JS looks on bicycle first — no describe() there
                                    // then walks up to vehicleBlueprint — found it!
console.log(sportsCar.describe());

// Confirm the chain
console.log(Object.getPrototypeOf(bicycle) === vehicleBlueprint); // true
console.log(Object.getPrototypeOf(vehicleBlueprint) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null — end of chain

// Checking OWN properties vs inherited properties
console.log(bicycle.hasOwnProperty('type'));      // true  — own property
console.log(bicycle.hasOwnProperty('describe'));  // false — inherited from vehicleBlueprint
▶ Output
I am a bicycle that travels at 30 km/h
I am a sports car that travels at 300 km/h
true
true
null
true
false
⚠️
Pro Tip:Always use Object.getPrototypeOf(obj) instead of obj.__proto__ in production code. __proto__ is a legacy accessor that was only standardised for web compatibility — getPrototypeOf is the clean, spec-approved way to inspect the chain.

Constructor Functions and .prototype — Building Inheritance Before ES6

Before ES6 classes, JavaScript developers used constructor functions to create objects with shared behaviour. A constructor function is just a regular function you call with the new keyword. When you do that, JavaScript does four things automatically: creates a new empty object, sets its [[Prototype]] to the constructor's .prototype property, runs the function body with this pointing to the new object, and returns that object.

This is where the .prototype property on functions becomes important. Every function in JavaScript automatically gets a .prototype object. When you add a method to that .prototype, every instance created by that constructor gets access to it through the chain — without storing their own copy.

The pattern for inheritance between two constructor functions requires one extra step: wiring up the prototype chain manually with Object.create(). If you forget this, instances of the child constructor won't be able to reach the parent's methods. This manual wiring is exactly what ES6 classes automate with the extends keyword — the underlying mechanism is identical.

Understanding this pattern isn't just history — you'll encounter it in older codebases regularly, and knowing it makes ES6 class behaviour completely transparent.

constructor-inheritance.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// --- Prototype-based inheritance using constructor functions ---

// PARENT constructor
function Animal(name, sound) {
  // These become OWN properties on each instance
  this.name = name;
  this.sound = sound;
}

// Shared method — added to Animal.prototype so ALL instances share one copy
Animal.prototype.speak = function() {
  return `${this.name} says ${this.sound}!`;
};

Animal.prototype.describe = function() {
  return `${this.name} is an animal.`;
};

// CHILD constructor
function Dog(name) {
  // Step 1: Call the parent constructor to set up own properties
  // Without this, 'name' and 'sound' would never be set on the Dog instance
  Animal.call(this, name, 'Woof');
  this.tricks = [];  // Dog-specific own property
}

// Step 2: Wire up the prototype chain
// Dog.prototype must have Animal.prototype in ITS chain
// Object.create() creates a new object whose [[Prototype]] is Animal.prototype
Dog.prototype = Object.create(Animal.prototype);

// Step 3: Fix the constructor reference (Object.create broke it)
// Without this, dog.constructor would point to Animal — wrong!
Dog.prototype.constructor = Dog;

// Dog-specific method — only Dogs have this, not Animals
Dog.prototype.learnTrick = function(trick) {
  this.tricks.push(trick);
  return `${this.name} learnt: ${trick}`;
};

// --- Usage ---
const myDog = new Dog('Rex');

console.log(myDog.speak());              // Found on Animal.prototype via the chain
console.log(myDog.describe());           // Also from Animal.prototype
console.log(myDog.learnTrick('sit'));    // Found on Dog.prototype
console.log(myDog.tricks);              // Own property on the instance

// Verifying the chain
console.log(myDog instanceof Dog);       // true
console.log(myDog instanceof Animal);    // true — chain reaches Animal.prototype
console.log(myDog.constructor === Dog);  // true — because we fixed it in Step 3

// What the chain looks like:
// myDog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null
▶ Output
Rex says Woof!
Rex is an animal.
Rex learnt: sit
[ 'sit' ]
true
true
true
⚠️
Watch Out:Never write Dog.prototype = Animal.prototype (without Object.create). If you do, adding methods to Dog.prototype will pollute Animal.prototype too — because they're the same object. Object.create() gives you a fresh object that delegates to Animal.prototype instead of being it.

ES6 Classes — Same Prototype Chain, Cleaner Syntax

ES6 classes aren't a new object system. They're a cleaner way to write the exact same constructor-function pattern you just saw. Under the hood, class produces a constructor function, and extends sets up the prototype chain using Object.create() just like we did manually. Don't let the keyword fool you into thinking JavaScript became a classical OOP language — it didn't.

The super keyword in a child class does two jobs. Inside the constructor, super(...args) calls the parent's constructor function — the equivalent of Animal.call(this, ...) that we wrote manually. Inside a method, super.methodName() climbs up the prototype chain and calls the parent's version of that method, which is how you extend behaviour rather than replace it.

One genuinely new thing classes give you is private fields (using the # prefix), which the prototype pattern never had. Private fields are stored directly on the instance and are invisible outside the class body — they're not on the prototype at all, which is why they can't be accessed via the chain.

Knowing both syntaxes is what makes you dangerous. You can read legacy code, you can explain what a transpiler like Babel actually outputs, and you'll never be confused by a class behaving 'weirdly' because you understand the prototype reality underneath.

class-inheritance.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// --- ES6 class inheritance — same prototype chain, readable syntax ---

class BankAccount {
  // Private field — NOT on the prototype, not accessible outside this class
  #balance;

  constructor(owner, initialDeposit) {
    this.owner = owner;          // own property
    this.#balance = initialDeposit;  // private own property
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('Deposit must be positive');
    this.#balance += amount;
    return this; // enables method chaining
  }

  getBalance() {
    return this.#balance;
  }

  toString() {
    return `${this.owner}'s account: $${this.#balance}`;
  }
}

// SavingsAccount EXTENDS BankAccount
// Under the hood: SavingsAccount.prototype = Object.create(BankAccount.prototype)
class SavingsAccount extends BankAccount {
  #interestRate;

  constructor(owner, initialDeposit, annualInterestRate) {
    // super() MUST be called before accessing 'this' in a derived class
    // It runs BankAccount's constructor on the new instance
    super(owner, initialDeposit);
    this.#interestRate = annualInterestRate;
  }

  applyInterest() {
    // getBalance() is inherited from BankAccount.prototype via the chain
    const interest = this.getBalance() * this.#interestRate;
    this.deposit(interest);  // deposit() also inherited
    return `Interest of $${interest.toFixed(2)} applied`;
  }

  // Override toString — but call the parent version inside it
  toString() {
    // super.toString() walks up to BankAccount.prototype.toString
    return `${super.toString()} [Savings @ ${this.#interestRate * 100}% interest]`;
  }
}

// --- Usage ---
const currentAccount = new BankAccount('Alice', 1000);
currentAccount.deposit(500);  // method chaining is possible because deposit returns this
console.log(currentAccount.getBalance());  // 1500
console.log(currentAccount.toString());

const savingsAccount = new SavingsAccount('Bob', 2000, 0.05);
console.log(savingsAccount.applyInterest());
console.log(savingsAccount.toString());

// instanceof still works — class inheritance IS prototype inheritance
console.log(savingsAccount instanceof SavingsAccount); // true
console.log(savingsAccount instanceof BankAccount);    // true

// The prototype chain is identical to the manual version:
// savingsAccount --> SavingsAccount.prototype --> BankAccount.prototype --> Object.prototype --> null
console.log(
  Object.getPrototypeOf(SavingsAccount.prototype) === BankAccount.prototype
); // true — confirms extends wired this up
▶ Output
1500
Alice's account: $1500
Interest of $100.00 applied
Bob's account: $2100 [Savings @ 5% interest]
true
true
true
🔥
Interview Gold:If an interviewer asks 'Are ES6 classes just syntactic sugar?' the precise answer is: mostly yes — they use the same prototype chain — but with one real addition: private fields (#name) are a genuinely new feature that the old constructor pattern couldn't replicate without closures.

Real-World Pattern — Composable Mixins Over Deep Inheritance

Here's the dirty secret senior engineers know: deep inheritance hierarchies are fragile. The moment your requirements change, the base class is impossible to modify without breaking every subclass. This is the 'gorilla banana problem' — you wanted a banana, but you inherited the gorilla holding it and the entire jungle it lives in.

The JavaScript prototype chain gives you a practical alternative: mixins. Instead of inheriting from a chain of parent classes, you compose objects by copying or delegating methods from multiple sources. This is idiomatic modern JavaScript and it sidesteps the rigidity of single-parent inheritance entirely.

The most common mixin pattern uses Object.assign() to copy methods onto a prototype, or uses a function that takes a superclass and returns an extended class (a 'class factory mixin'). This lets you snap in capabilities like Serializable, Timestamped, or Auditable without creating a brittle hierarchy.

This matters in the real world because UI component libraries, state management systems, and ORMs all use composition patterns like this. Recognising them — and knowing when to choose composition over inheritance — is what separates intermediate developers from senior ones.

mixins-composition.js · JAVASCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// --- Mixins: composing behaviour without deep inheritance ---

// Mixin factory: a function that takes a Base class and returns an extended class
// This lets us 'snap in' abilities without a fixed hierarchy

const Serialisable = (BaseClass) => class extends BaseClass {
  toJSON() {
    // Serialise all own enumerable properties to a JSON string
    const ownProps = {};
    for (const key of Object.keys(this)) {
      ownProps[key] = this[key];
    }
    return JSON.stringify(ownProps);
  }

  static fromJSON(json) {
    const data = JSON.parse(json);
    // Creates an instance and copies all properties on to it
    return Object.assign(new this(), data);
  }
};

const Timestamped = (BaseClass) => class extends BaseClass {
  constructor(...args) {
    super(...args);
    // Automatically stamp with created time when constructed
    this.createdAt = new Date().toISOString();
  }

  getAge() {
    const ms = Date.now() - new Date(this.createdAt).getTime();
    return `${Math.floor(ms / 1000)} seconds old`;
  }
};

// The base class stays lean — only core identity
class User {
  constructor(username, email) {
    this.username = username;
    this.email = email;
  }

  greet() {
    return `Hello, I'm ${this.username}`;
  }
}

// Compose: User + Timestamped + Serialisable
// Read right to left: start with User, add Timestamped, add Serialisable
class PlatformUser extends Serialisable(Timestamped(User)) {
  constructor(username, email, role) {
    super(username, email);  // chains up through all the mixins to User
    this.role = role;
  }
}

// --- Usage ---
const admin = new PlatformUser('sarah_dev', 'sarah@example.com', 'admin');

console.log(admin.greet());          // from User
console.log(admin.getAge());         // from Timestamped mixin
console.log(admin.toJSON());         // from Serialisable mixin

// instanceof still works for all layers
console.log(admin instanceof User);  // true
console.log(admin instanceof PlatformUser); // true

// The prototype chain records every mixin link:
// admin
//   --> PlatformUser.prototype
//   --> Serialisable(Timestamped(User)).prototype  [anonymous class]
//   --> Timestamped(User).prototype               [anonymous class]
//   --> User.prototype
//   --> Object.prototype
//   --> null
▶ Output
Hello, I'm sarah_dev
0 seconds old
{"username":"sarah_dev","email":"sarah@example.com","createdAt":"2024-01-15T10:30:00.000Z","role":"admin"}
true
true
⚠️
Pro Tip:Prefer composition over inheritance when a behaviour could reasonably appear on unrelated classes — for example, both a BlogPost and a UserProfile might need toJSON(), but they shouldn't share a parent. That's exactly when a mixin earns its keep.
AspectConstructor Functions (.prototype)ES6 Classes (class / extends)
Syntax clarityVerbose — chain wiring is manualClean — extends and super handle it
Prototype chainIdentical underlying mechanismIdentical underlying mechanism
Private stateRequires closure workaroundNative private fields with # prefix
Calling parent constructorAnimal.call(this, ...args)super(...args) — must be first
Calling parent methodAnimal.prototype.method.call(this)super.method()
HoistingFunction declarations are hoistedClasses are NOT hoisted — use after declaration
Strict modeOptionalAlways in strict mode automatically
Seen in codebasesLegacy and transpiled output (Babel)Modern JS, TypeScript, frameworks

🎯 Key Takeaways

  • The prototype chain is a linked list of objects — property lookup walks up it automatically, which is why .map() exists on every array without you defining it.
  • Constructor function .prototype and instance __proto__ are different things: .prototype is a property on functions that becomes the [[Prototype]] of instances; __proto__ (or Object.getPrototypeOf) is how you read that link on an instance.
  • ES6 classes compile down to the same constructor-function + prototype pattern — 'extends' runs Object.create() and 'super()' runs the parent constructor. The chain is identical.
  • Deep inheritance hierarchies are fragile — class factory mixins let you compose reusable abilities (Serialisable, Timestamped) onto unrelated classes without forcing them into a single hierarchy.

⚠ Common Mistakes to Avoid

  • Mistake 1: Adding methods directly to the instance inside the constructor (this.greet = function(){}) instead of on the prototype — Symptom: every instance carries its own copy of the function, wasting memory. You can verify it by checking instance.hasOwnProperty('greet') returning true — Fix: move the method to ClassName.prototype.greet or keep using ES6 classes where methods are automatically placed on the prototype.
  • Mistake 2: Forgetting to call super() before using 'this' in a derived class constructor — Symptom: ReferenceError: Must call super constructor in derived class before accessing 'this' — Fix: always call super(...args) as the very first statement in the child class constructor. JavaScript enforces this because the parent constructor is responsible for initialising the object that 'this' will point to.
  • Mistake 3: Mutating a shared array or object on the prototype — Symptom: pushing to one instance's array updates every instance's array, which looks like a ghost-write bug — Fix: initialise arrays and objects inside the constructor (this.items = []) so each instance gets its own copy, not a shared reference on the prototype. Prototypes should only hold methods, never mutable state.

Interview Questions on This Topic

  • QWhat is the difference between __proto__ and .prototype in JavaScript? Can you draw out the relationship between a constructor function, its .prototype property, and an instance created from it?
  • QWhat does the 'new' keyword actually do step by step? If you had to implement your own version of 'new' as a function, how would you write it?
  • QES6 classes are often called 'syntactic sugar over prototypes' — do you agree with that statement entirely, or are there things classes do that the old constructor-function pattern genuinely cannot?

Frequently Asked Questions

What is the prototype chain in JavaScript?

The prototype chain is the mechanism JavaScript uses to look up properties and methods. Every object has a hidden [[Prototype]] link to another object. When you access a property, JS first checks the object itself, then walks up the chain through each linked object until it either finds the property or reaches null at the end of the chain.

What is the difference between classical inheritance and prototypal inheritance?

Classical inheritance (Java, C++) copies a blueprint — the class — into new instances at compile time. Prototypal inheritance (JavaScript) links objects together at runtime. Instances don't copy methods; they delegate to their prototype. This means you can modify the prototype after instances are created and all instances immediately see the change.

Does using ES6 classes mean JavaScript is no longer prototype-based?

No. ES6 classes are syntax on top of the same prototype system. Running class Dog extends Animal creates a constructor function and wires Dog.prototype to Animal.prototype using Object.create() — exactly as you'd do manually. You can verify this by checking Object.getPrototypeOf(Dog.prototype) === Animal.prototype, which returns true for any class hierarchy.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousEvent Loop in JavaScriptNext →ES6+ Features in JavaScript
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged