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
