Prototypes & Inheritance

Prototype chain, prototypal inheritance, and Object.create

Understanding Prototypes

JavaScript uses prototypal inheritance, not classical inheritance like Java or C++. Every object in JavaScript has an internal link to another object called its prototype. When you access a property, JavaScript first looks on the object itself, then walks up the prototype chain.

Key Concepts

  • [[Prototype]] — Internal link to the prototype object (accessed via __proto__ or Object.getPrototypeOf())
  • prototype — Property on constructor functions that becomes [[Prototype]] of instances
  • Prototype Chain — The chain of prototypes JavaScript searches for properties

The Prototype Chain

const animal = {
  eat() {
    console.log("Eating...");
  }
};

const dog = {
  bark() {
    console.log("Woof!");
  }
};

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

dog.bark(); // "Woof!" — found on dog
dog.eat();  // "Eating..." — found on dog's prototype (animal)

// The chain: dog → animal → Object.prototype → null
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

Constructor Functions & prototype

Before ES6 classes, constructor functions were the primary way to create objects with shared behavior:

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

// Methods go on the prototype (shared across all instances)
Person.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};

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

const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);

console.log(alice.greet()); // "Hi, I'm Alice"
console.log(bob.greet());   // "Hi, I'm Bob"

// Both share the same method
console.log(alice.greet === bob.greet); // true

// Check the prototype chain
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

Object.create()

Create objects with a specific prototype without using constructors:

const vehiclePrototype = {
  start() {
    console.log(`${this.type} starting...`);
  },
  stop() {
    console.log(`${this.type} stopping...`);
  }
};

// Create object with vehiclePrototype as its prototype
const car = Object.create(vehiclePrototype);
car.type = "Car";
car.wheels = 4;

car.start(); // "Car starting..."

// Create with properties defined
const motorcycle = Object.create(vehiclePrototype, {
  type: { value: "Motorcycle", writable: true },
  wheels: { value: 2, writable: true }
});

motorcycle.start(); // "Motorcycle starting..."

Inheritance with Constructor Functions

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

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

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

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add Dog-specific methods
Dog.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};

// Override parent method
Dog.prototype.speak = function() {
  console.log(`${this.name} says woof!`);
};

const rex = new Dog("Rex", "German Shepherd");
rex.speak(); // "Rex says woof!"
rex.bark();  // "Rex barks!"

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

ES6 Classes (Syntactic Sugar)

ES6 classes are syntactic sugar over prototype-based inheritance:

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
  
  speak() {
    console.log(`${this.name} barks!`);
  }
  
  fetch() {
    console.log(`${this.name} fetches the ball`);
  }
}

const buddy = new Dog("Buddy", "Labrador");
buddy.speak(); // "Buddy barks!"

// Under the hood, it's still prototypes
console.log(Dog.prototype.__proto__ === Animal.prototype); // true

Useful Prototype Methods

const obj = { a: 1 };

// Check prototype
Object.getPrototypeOf(obj); // Object.prototype

// Set prototype (avoid in production - slow)
Object.setPrototypeOf(obj, null);

// Check if property is own (not inherited)
obj.hasOwnProperty("a"); // true

// Check if object is in prototype chain
Animal.prototype.isPrototypeOf(buddy); // true

// Get own property names
Object.keys(obj);            // ["a"] (enumerable)
Object.getOwnPropertyNames(obj); // ["a"] (all own props)

// Check instance
buddy instanceof Dog;        // true
buddy instanceof Animal;     // true

⚠️ Common Pitfalls

  • Modifying built-in prototypes: Don't add methods to Array.prototype, Object.prototype, etc.
  • Forgetting new: Without new, constructor functions don't create objects properly
  • Arrow functions: They don't have their own prototype property
  • Performance: Long prototype chains can slow down property lookups

💡 Key Takeaways

  • • Every object has a [[Prototype]] linking to another object
  • • Property lookups traverse the prototype chain until found or null
  • • Constructor functions use .prototype to share methods
  • • Use Object.create() for clean prototype-based inheritance
  • • ES6 classes are syntactic sugar over prototypes
  • • Prefer composition over deep inheritance hierarchies