Chapter 11: The Prototype and Inheritance Saga
· 14 min read
Chapter 11: The Prototype and Inheritance Saga
In JavaScript Land, objects can inherit from other objects through a mysterious chain called the prototype chain.
Understanding Prototypes
// Every function has a prototype property
function Hero(name, power) {
this.name = name;
this.power = power;
}
// Add methods to the prototype
Hero.prototype.introduce = function() {
return `I am ${this.name}, and I have ${this.power}!`;
};
Hero.prototype.fight = function(villain) {
return `${this.name} fights ${villain} using ${this.power}!`;
};
// Create instances
const superman = new Hero("Superman", "super strength");
const batman = new Hero("Batman", "intellect and gadgets");
console.log(superman.introduce()); // "I am Superman, and I have super strength!"
console.log(batman.fight("Joker")); // "Batman fights Joker using intellect and gadgets!"
// All instances share the same methods
console.log(superman.introduce === batman.introduce); // true
// The prototype chain in action
console.log(superman.hasOwnProperty("name")); // true (own property)
console.log(superman.hasOwnProperty("introduce")); // false (inherited)
console.log("introduce" in superman); // true (found in chain)
The Prototype Chain
// Every object has a __proto__ property pointing to its prototype
console.log(superman.__proto__ === Hero.prototype); // true
console.log(Hero.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of chain)
// Property lookup walks up the chain
const obj = {
name: "Test"
};
console.log(obj.toString()); // Found in Object.prototype
console.log(obj.hasOwnProperty("name")); // Found in Object.prototype
// console.log(obj.nonExistent); // undefined (not found anywhere in chain)
// Prototype pollution (be careful!)
Object.prototype.hackedMethod = function() {
return "I shouldn't be here!";
};
console.log(superman.hackedMethod()); // "I shouldn't be here!"
// Every object now has this method!
delete Object.prototype.hackedMethod; // Clean up
Classical Inheritance Patterns
// Constructor inheritance
function Superhero(name, power, secretIdentity) {
Hero.call(this, name, power); // Call parent constructor
this.secretIdentity = secretIdentity;
}
// Set up prototype inheritance
Superhero.prototype = Object.create(Hero.prototype);
Superhero.prototype.constructor = Superhero;
// Add specialized methods
Superhero.prototype.revealIdentity = function() {
return `My secret identity is ${this.secretIdentity}!`;
};
// Override parent method
Superhero.prototype.introduce = function() {
return Hero.prototype.introduce.call(this) + " I'm a superhero!";
};
const spiderman = new Superhero("Spider-Man", "web-slinging", "Peter Parker");
console.log(spiderman.introduce()); // Calls overridden method
console.log(spiderman.revealIdentity()); // "My secret identity is Peter Parker!"
console.log(spiderman.fight("Green Goblin")); // Inherited from Hero
Modern Class Syntax (ES6+)
// Classes are just syntactic sugar over prototypes
class ModernHero {
constructor(name, power) {
this.name = name;
this.power = power;
}
introduce() {
return `I am ${this.name}, and I have ${this.power}!`;
}
fight(villain) {
return `${this.name} fights ${villain} using ${this.power}!`;
}
// Static methods belong to the class, not instances
static compareHeroes(hero1, hero2) {
return `${hero1.name} vs ${hero2.name}`;
}
}
// Inheritance with extends
class ModernSuperhero extends ModernHero {
constructor(name, power, secretIdentity) {
super(name, power); // Call parent constructor
this.secretIdentity = secretIdentity;
}
revealIdentity() {
return `My secret identity is ${this.secretIdentity}!`;
}
// Override parent method
introduce() {
return super.introduce() + " I'm a superhero!";
}
// Getter and setter
get identity() {
return this.secretIdentity;
}
set identity(newIdentity) {
this.secretIdentity = newIdentity;
}
}
const modernSpiderman = new ModernSuperhero("Spider-Man", "web-slinging", "Peter Parker");
console.log(modernSpiderman.introduce());
console.log(modernSpiderman.identity); // Using getter
modernSpiderman.identity = "Miles Morales"; // Using setter
// Static method usage
console.log(ModernHero.compareHeroes(superman, modernSpiderman));
Advanced Prototype Patterns
Mixins - Multiple Inheritance Alternative
// JavaScript doesn't support multiple inheritance, but we can use mixins
const CanFly = {
fly() {
return `${this.name} is flying!`;
},
land() {
return `${this.name} has landed.`;
}
};
const CanSwim = {
swim() {
return `${this.name} is swimming!`;
},
dive() {
return `${this.name} dives underwater.`;
}
};
// Mixin function
function mixin(target, ...sources) {
Object.assign(target.prototype, ...sources);
return target;
}
class Aquaman extends Hero {
constructor(name) {
super(name, "underwater breathing");
}
}
// Apply mixins
mixin(Aquaman, CanSwim);
const aquaman = new Aquaman("Aquaman");
console.log(aquaman.swim()); // "Aquaman is swimming!"
console.log(aquaman.introduce()); // Still has Hero methods
Factory Functions with Prototypes
// Alternative to classes - factory functions
function createAnimal(type, name) {
const animal = Object.create(animalPrototype);
animal.type = type;
animal.name = name;
return animal;
}
const animalPrototype = {
speak() {
return `${this.name} the ${this.type} makes a sound.`;
},
move() {
return `${this.name} is moving.`;
}
};
const dog = createAnimal("dog", "Buddy");
console.log(dog.speak()); // "Buddy the dog makes a sound."
Private Properties with WeakMaps
// True privacy using WeakMap
const privateProps = new WeakMap();
class SecureHero {
constructor(name, secretWeapon) {
this.name = name;
privateProps.set(this, { secretWeapon });
}
useSecretWeapon() {
const { secretWeapon } = privateProps.get(this);
return `${this.name} uses ${secretWeapon}!`;
}
}
const ironman = new SecureHero("Iron Man", "Arc Reactor");
console.log(ironman.useSecretWeapon()); // "Iron Man uses Arc Reactor!"
console.log(ironman.secretWeapon); // undefined - truly private!
Understanding instanceof and Prototype Checks
// instanceof checks the prototype chain
console.log(spiderman instanceof Superhero); // true
console.log(spiderman instanceof Hero); // true
console.log(spiderman instanceof Object); // true
// isPrototypeOf checks if object is in prototype chain
console.log(Hero.prototype.isPrototypeOf(spiderman)); // true
console.log(Superhero.prototype.isPrototypeOf(spiderman)); // true
// Object.getPrototypeOf gets the prototype
console.log(Object.getPrototypeOf(spiderman) === Superhero.prototype); // true
// Custom instanceof behavior with Symbol.hasInstance
class SpecialClass {
static [Symbol.hasInstance](instance) {
return instance.isSpecial === true;
}
}
const normalObj = { isSpecial: false };
const specialObj = { isSpecial: true };
console.log(normalObj instanceof SpecialClass); // false
console.log(specialObj instanceof SpecialClass); // true
Prototype Performance Considerations
// Method on prototype (memory efficient)
function EfficientClass() {
this.data = [];
}
EfficientClass.prototype.processData = function() {
// This method exists once in memory
return this.data.map(x => x * 2);
};
// Method in constructor (memory inefficient)
function InefficientClass() {
this.data = [];
// New function created for each instance!
this.processData = function() {
return this.data.map(x => x * 2);
};
}
// Compare memory usage
const efficient1 = new EfficientClass();
const efficient2 = new EfficientClass();
console.log(efficient1.processData === efficient2.processData); // true (same function)
const inefficient1 = new InefficientClass();
const inefficient2 = new InefficientClass();
console.log(inefficient1.processData === inefficient2.processData); // false (different functions)
Common Prototype Pitfalls
// Pitfall 1: Modifying built-in prototypes
// ❌ Don't do this!
Array.prototype.myCustomMethod = function() {
return "This pollutes all arrays!";
};
// Pitfall 2: Forgetting to reset constructor
function Parent() {}
function Child() {}
// ❌ Wrong
Child.prototype = Object.create(Parent.prototype);
// Constructor now points to Parent!
// ✅ Correct
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// Pitfall 3: Shared reference properties
function BadDesign() {}
BadDesign.prototype.sharedArray = []; // ❌ All instances share this array!
const bad1 = new BadDesign();
const bad2 = new BadDesign();
bad1.sharedArray.push("oops");
console.log(bad2.sharedArray); // ["oops"] - Unintended sharing!
// ✅ Better approach
function GoodDesign() {
this.ownArray = []; // Each instance gets its own array
}
ES6+ Class Features
// Private fields (ES2022)
class ModernClass {
#privateField = 42;
#privateMethod() {
return "This is private!";
}
publicMethod() {
return this.#privateMethod() + ` Private field: ${this.#privateField}`;
}
}
const modern = new ModernClass();
console.log(modern.publicMethod()); // Works
// console.log(modern.#privateField); // SyntaxError
// Static blocks for complex initialization
class ComplexClass {
static #database;
static {
// Runs once when class is defined
this.#database = new Map();
this.#database.set("admin", { role: "superuser" });
}
static getUser(username) {
return this.#database.get(username);
}
}
Next Chapter: Chapter 12: Advanced Concepts and Modern Features
Previous Chapter: Chapter 10: The Asynchronous Adventures
Table of Contents: JavaScript Guide