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​