Skip to main content

Module 21: Prototypes and Inheritance

Prototypes are the mechanism by which JavaScript objects inherit features from one another. Understanding prototypes is crucial for mastering JavaScript's object system.


1. Understanding Prototypes

1.1 What is a Prototype?

Every JavaScript object has an internal property called [[Prototype]] (accessible via __proto__ or Object.getPrototypeOf()).

const obj = { name: 'John' };

console.log(obj.__proto__); // Object.prototype
console.log(Object.getPrototypeOf(obj)); // Object.prototype
console.log(obj.__proto__ === Object.prototype); // true

1.2 Prototype Chain

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

const dog = {
barks: true
};

// Set prototype
Object.setPrototypeOf(dog, animal);

console.log(dog.eats); // true (inherited from animal)
console.log(dog.barks); // true (own property)
dog.walk(); // "Animal walks" (inherited method)

// Prototype chain: dog → animal → Object.prototype → null
Prototype Chain

When accessing a property, JavaScript looks up the prototype chain until it finds the property or reaches null.


2. Constructor Functions and Prototypes

2.1 Constructor Functions

function Person(name, age) {
this.name = name;
this.age = age;
}

// Add methods to prototype
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};

Person.prototype.birthday = function() {
this.age++;
};

const john = new Person('John', 30);
john.greet(); // "Hello, I'm John"

console.log(john.__proto__ === Person.prototype); // true

2.2 Why Use Prototypes?

// ❌ Bad: Methods in constructor (created for each instance)
function Person(name) {
this.name = name;
this.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
}

const p1 = new Person('John');
const p2 = new Person('Jane');
console.log(p1.greet === p2.greet); // false (different functions!)

// ✅ Good: Methods on prototype (shared by all instances)
function Person(name) {
this.name = name;
}

Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};

const p3 = new Person('John');
const p4 = new Person('Jane');
console.log(p3.greet === p4.greet); // true (same function!)

3. Prototype Methods

3.1 Object.create()

const animal = {
eats: true,
walk() {
console.log('Walking...');
}
};

// Create object with animal as prototype
const rabbit = Object.create(animal);
rabbit.jumps = true;

console.log(rabbit.eats); // true (inherited)
console.log(rabbit.jumps); // true (own property)
rabbit.walk(); // "Walking..." (inherited)

3.2 Object.getPrototypeOf() / Object.setPrototypeOf()

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

// Get prototype
console.log(Object.getPrototypeOf(dog)); // Object.prototype

// Set prototype
Object.setPrototypeOf(dog, animal);
console.log(Object.getPrototypeOf(dog)); // animal
console.log(dog.eats); // true

3.3 hasOwnProperty()

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

console.log(rabbit.hasOwnProperty('jumps')); // true
console.log(rabbit.hasOwnProperty('eats')); // false (inherited)

// Check if property exists (own or inherited)
console.log('eats' in rabbit); // true
console.log('jumps' in rabbit); // true

4. Prototypal Inheritance

4.1 Basic Inheritance

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

Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};

function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor
this.breed = breed;
}

// Inherit from Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
console.log(`${this.name} barks!`);
};

const dog = new Dog('Rex', 'Labrador');
dog.eat(); // "Rex is eating" (inherited)
dog.bark(); // "Rex barks!" (own method)

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

4.2 Multiple Levels of Inheritance

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

Animal.prototype.eat = function() {
console.log(`${this.name} eats`);
};

function Mammal(name, furColor) {
Animal.call(this, name);
this.furColor = furColor;
}

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;

Mammal.prototype.nurse = function() {
console.log(`${this.name} nurses its young`);
};

function Dog(name, furColor, breed) {
Mammal.call(this, name, furColor);
this.breed = breed;
}

Dog.prototype = Object.create(Mammal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};

const dog = new Dog('Rex', 'brown', 'Labrador');
dog.eat(); // Inherited from Animal
dog.nurse(); // Inherited from Mammal
dog.bark(); // Own method

5. ES6 Classes vs Prototypes

5.1 Class Syntax

class Animal {
constructor(name) {
this.name = name;
}

eat() {
console.log(`${this.name} is eating`);
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}

bark() {
console.log(`${this.name} barks`);
}
}

5.2 Prototype Equivalent

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

Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};

function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};
Classes Are Syntactic Sugar

ES6 classes are built on top of prototypes. Understanding prototypes helps you understand what classes are really doing.


6. Prototype Properties

6.1 constructor Property

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

console.log(Person.prototype.constructor === Person); // true

const john = new Person('John');
console.log(john.constructor === Person); // true

// Fixing constructor after inheritance
function Dog(name) {
this.name = name;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix constructor reference

6.2 Prototype vs Instance Properties

function Person(name) {
this.name = name; // Instance property
}

Person.prototype.species = 'Human'; // Prototype property

const john = new Person('John');
const jane = new Person('Jane');

console.log(john.name); // "John" (instance)
console.log(jane.name); // "Jane" (instance)
console.log(john.species); // "Human" (prototype)
console.log(jane.species); // "Human" (prototype)

// Modify prototype property
Person.prototype.species = 'Homo Sapiens';
console.log(john.species); // "Homo Sapiens" (affects all instances)

// Override in instance
john.species = 'Modified';
console.log(john.species); // "Modified" (own property)
console.log(jane.species); // "Homo Sapiens" (still uses prototype)

7. Built-in Prototypes

7.1 Object.prototype

const obj = {};

// Methods from Object.prototype
console.log(obj.toString());
console.log(obj.hasOwnProperty('name'));
console.log(obj.valueOf());

// Chain: obj → Object.prototype → null
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

7.2 Array.prototype

const arr = [1, 2, 3];

// Chain: arr → Array.prototype → Object.prototype → null
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true

// Array methods from Array.prototype
arr.push(4);
arr.forEach(n => console.log(n));

7.3 Function.prototype

function myFunc() {}

// Chain: myFunc → Function.prototype → Object.prototype → null
console.log(Object.getPrototypeOf(myFunc) === Function.prototype); // true

// Function methods
console.log(myFunc.call);
console.log(myFunc.apply);
console.log(myFunc.bind);

7.4 Extending Built-in Prototypes

// Adding custom methods (use with caution!)
Array.prototype.last = function() {
return this[this.length - 1];
};

const arr = [1, 2, 3];
console.log(arr.last()); // 3
Don't Modify Built-in Prototypes

Extending built-in prototypes can cause conflicts and unexpected behavior. Use utility functions or subclasses instead.


8. Practical Patterns

8.1 Mixin Pattern

const canEat = {
eat() {
console.log(`${this.name} is eating`);
}
};

const canWalk = {
walk() {
console.log(`${this.name} is walking`);
}
};

const canSwim = {
swim() {
console.log(`${this.name} is swimming`);
}
};

// Mix behaviors into prototype
function mixin(target, ...sources) {
Object.assign(target, ...sources);
}

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

mixin(Animal.prototype, canEat, canWalk);

function Fish(name) {
Animal.call(this, name);
}

Fish.prototype = Object.create(Animal.prototype);
mixin(Fish.prototype, canSwim);

const fish = new Fish('Nemo');
fish.eat(); // "Nemo is eating"
fish.swim(); // "Nemo is swimming"

8.2 Factory Pattern with Prototypes

function createAnimal(name, type) {
const animal = Object.create(animalMethods);
animal.name = name;
animal.type = type;
return animal;
}

const animalMethods = {
eat() {
console.log(`${this.name} is eating`);
},
sleep() {
console.log(`${this.name} is sleeping`);
}
};

const dog = createAnimal('Rex', 'dog');
const cat = createAnimal('Whiskers', 'cat');

dog.eat(); // "Rex is eating"
cat.eat(); // "Whiskers is eating"

9. Performance Considerations

9.1 Property Lookup

// Faster: Own property
const obj = { name: 'John' };
console.log(obj.name); // Fast (own property)

// Slower: Deep prototype chain
const deep = Object.create(
Object.create(
Object.create({ name: 'John' })
)
);
console.log(deep.name); // Slower (3 lookups)

9.2 Caching Lookups

function Person(name) {
this.name = name;
// Cache prototype method reference
this._greet = Person.prototype.greet;
}

Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};

10. Modern Alternatives

10.1 Object Composition

// Instead of inheritance, compose objects
const canEat = (state) => ({
eat: () => console.log(`${state.name} is eating`)
});

const canWalk = (state) => ({
walk: () => console.log(`${state.name} is walking`)
});

function createDog(name) {
const state = { name };
return {
...canEat(state),
...canWalk(state),
bark: () => console.log(`${name} barks`)
};
}

const dog = createDog('Rex');
dog.eat();
dog.walk();
dog.bark();
Composition Over Inheritance

Modern JavaScript favors composition over classical inheritance for flexibility and maintainability.


Summary

In this module, you learned:

  • ✅ What prototypes are and how they work
  • ✅ Prototype chain and property lookup
  • ✅ Constructor functions and prototypes
  • ✅ Prototypal inheritance patterns
  • ✅ ES6 classes vs prototypes
  • ✅ Built-in prototypes
  • ✅ Mixin and factory patterns
  • ✅ Performance considerations
  • ✅ Modern alternatives like composition
Next Steps

In Module 22, you'll learn about Functional Programming concepts in JavaScript.


Practice Exercises

  1. Implement inheritance using prototypes
  2. Create a mixin system for behaviors
  3. Build a prototype-based class hierarchy
  4. Compare prototype vs class performance
  5. Extend built-in prototypes safely
  6. Implement object composition patterns
  7. Create a factory function system
  8. Debug prototype chain issues

Additional Resources