JavaScript Prototypes - Complete Guide

JavaScript prototypes are the mechanism for inheritance and object property lookup. This guide explains prototypes, the prototype chain, and how inheritance works.

What is a Prototype? #

Every JavaScript object has an internal property [[Prototype]] that references another object. This creates a chain used for property lookup.

const obj = { name: 'Alice' };

// Access prototype
console.log(Object.getPrototypeOf(obj));  // Object.prototype

// Check if property exists on object (not prototype)
console.log(obj.hasOwnProperty('name'));  // true
console.log(obj.hasOwnProperty('toString'));  // false

Prototype Chain #

When accessing a property, JavaScript searches:

  1. The object itself
  2. The object’s prototype
  3. The prototype’s prototype
  4. Until reaching null
const animal = {
  eats: true,
  walk() {
    console.log('Walking');
  }
};

const dog = Object.create(animal);
dog.barks = true;

console.log(dog.eats);   // true (from animal prototype)
console.log(dog.barks);  // true (own property)

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

Constructor Functions #

Functions can create objects with shared prototypes.

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

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

Person.prototype.getAge = function() {
  return this.age;
};

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

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

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

What ’new’ Does #

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

const alice = new Person('Alice');

// Equivalent to:
const alice = {};
Object.setPrototypeOf(alice, Person.prototype);
Person.call(alice, 'Alice');

Object.create() #

Create objects with specific prototype.

const personPrototype = {
  greet() {
    return `Hello, I'm ${this.name}`;
  },

  getAge() {
    return this.age;
  }
};

const alice = Object.create(personPrototype);
alice.name = 'Alice';
alice.age = 25;

console.log(alice.greet());  // Hello, I'm Alice

Object.create() vs Constructor #

// Constructor function
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const alice = new Person('Alice');

// Equivalent with Object.create
const personPrototype = {
  greet() {
    return `Hello, ${this.name}`;
  }
};

function createPerson(name) {
  const person = Object.create(personPrototype);
  person.name = name;
  return person;
}

const bob = createPerson('Bob');

Prototype Properties #

prototype vs proto #

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

const alice = new Person('Alice');

// Function's prototype property (blueprint)
console.log(Person.prototype);

// Object's actual prototype (__proto__ or [[Prototype]])
console.log(alice.__proto__ === Person.prototype);  // true

// Better way to access prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype);  // true

Constructor Property #

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

const alice = new Person('Alice');

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

// Can create new instances
const bob = new alice.constructor('Bob');

Prototypal Inheritance #

Basic Inheritance #

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

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

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

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

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

const dog = new Dog('Max', 'Golden Retriever');
dog.speak();  // Max makes a sound
dog.bark();   // Max barks

Multiple Levels of Inheritance #

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

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

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

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

Mammal.prototype.warmBlooded = function() {
  console.log(`${this.name} is warm-blooded`);
};

function Dog(name, breed) {
  Mammal.call(this, name);
  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('Max', 'Labrador');
dog.eat();           // Max is eating (from Animal)
dog.warmBlooded();   // Max is warm-blooded (from Mammal)
dog.bark();          // Max barks (from Dog)

Checking Prototypes #

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

const alice = new Person('Alice');

// Check if prototype exists in chain
console.log(Person.prototype.isPrototypeOf(alice));  // true
console.log(Object.prototype.isPrototypeOf(alice));  // true

// instanceof
console.log(alice instanceof Person);  // true
console.log(alice instanceof Object);  // true

// Get prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype);  // true

// Has own property
console.log(alice.hasOwnProperty('name'));  // true
console.log(alice.hasOwnProperty('greet'));  // false

Modifying Prototypes #

Adding Methods #

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

const alice = new Person('Alice');

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

// Existing instances get new method
console.log(alice.greet());  // Hello, I'm Alice

Extending Built-in Prototypes #

// Add method to Array prototype
Array.prototype.last = function() {
  return this[this.length - 1];
};

const arr = [1, 2, 3, 4, 5];
console.log(arr.last());  // 5

// WARNING: Generally avoid modifying built-in prototypes
// Can cause conflicts with future JavaScript versions

Object Property Enumeration #

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

Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const alice = new Person('Alice', 25);

// for...in loops through own and inherited properties
for (let key in alice) {
  console.log(key);  // name, age, greet
}

// Only own properties
for (let key in alice) {
  if (alice.hasOwnProperty(key)) {
    console.log(key);  // name, age
  }
}

// Object.keys - only own enumerable properties
console.log(Object.keys(alice));  // ['name', 'age']

// Object.getOwnPropertyNames - all own properties
console.log(Object.getOwnPropertyNames(alice));  // ['name', 'age']

Prototype vs Class #

Modern JavaScript classes are syntactic sugar over prototypes.

// Class syntax
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// Equivalent with prototypes
function Person(name, age) {
  this.name = name;
  this.age = age;
}

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

Class Inheritance #

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

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

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

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

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

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

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`);
};

Practical Examples #

Mixin Pattern #

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

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

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

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

// Mix in abilities
Object.assign(Person.prototype, canEat, canWalk);

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

Object.assign(Fish.prototype, canEat, canSwim);

const alice = new Person('Alice');
alice.eat('pizza');  // Alice is eating pizza
alice.walk();        // Alice is walking

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

Method Delegation #

const arrayMethods = {
  push(item) {
    this.items.push(item);
    console.log(`Added ${item}`);
  },

  pop() {
    const item = this.items.pop();
    console.log(`Removed ${item}`);
    return item;
  },

  get length() {
    return this.items.length;
  }
};

function Stack() {
  this.items = [];
}

Object.setPrototypeOf(Stack.prototype, arrayMethods);

const stack = new Stack();
stack.push(1);  // Added 1
stack.push(2);  // Added 2
stack.pop();    // Removed 2

Prototype-based Factory #

const vehiclePrototype = {
  start() {
    console.log(`${this.name} started`);
  },

  stop() {
    console.log(`${this.name} stopped`);
  }
};

function createVehicle(name, type) {
  const vehicle = Object.create(vehiclePrototype);
  vehicle.name = name;
  vehicle.type = type;
  return vehicle;
}

const car = createVehicle('Toyota', 'car');
const bike = createVehicle('Honda', 'motorcycle');

car.start();   // Toyota started
bike.start();  // Honda started

Performance Considerations #

Shared Methods #

// Bad - New function for each instance
function Person(name) {
  this.name = name;
  this.greet = function() {
    return `Hello, ${this.name}`;
  };
}

const alice = new Person('Alice');
const bob = new Person('Bob');

console.log(alice.greet === bob.greet);  // false (wasteful)

// Good - Shared on prototype
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const alice = new Person('Alice');
const bob = new Person('Bob');

console.log(alice.greet === bob.greet);  // true (efficient)

Property Lookup #

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

Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const alice = new Person('Alice');

// Fast - Own property
console.log(alice.name);

// Slower - Prototype lookup
console.log(alice.greet);

// Cache if accessed frequently
const greet = alice.greet;
console.log(greet.call(alice));

Common Pitfalls #

Losing ’this’ Context #

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

Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const alice = new Person('Alice');
const greet = alice.greet;

console.log(greet());  // Hello, undefined

// Fix with bind
const boundGreet = alice.greet.bind(alice);
console.log(boundGreet());  // Hello, Alice

Overwriting Prototype #

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

Person.prototype.greet = function() {
  return 'Hello';
};

const alice = new Person('Alice');

// Bad - Breaks existing instances
Person.prototype = {
  greet() {
    return 'Hi';
  }
};

console.log(alice.greet());  // Still "Hello"

const bob = new Person('Bob');
console.log(bob.greet());  // "Hi"

Prototype Pollution #

// Dangerous - Can affect all objects
Object.prototype.polluted = true;

const obj = {};
console.log(obj.polluted);  // true

// Avoid modifying Object.prototype
delete Object.prototype.polluted;

Modern Alternatives #

Object.setPrototypeOf #

const animal = {
  speak() {
    console.log('Sound');
  }
};

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

Object.setPrototypeOf(dog, animal);

dog.bark();   // Woof
dog.speak();  // Sound

Object.assign #

const defaults = {
  theme: 'light',
  language: 'en'
};

const userSettings = Object.assign({}, defaults, {
  theme: 'dark'
});

console.log(userSettings);
// { theme: 'dark', language: 'en' }

Best Practices #

  1. Use classes for new code - Cleaner syntax
  2. Don’t modify built-in prototypes - Can cause conflicts
  3. Put methods on prototype - Memory efficient
  4. Use Object.create for inheritance - Clear intent
  5. Check with hasOwnProperty - Distinguish own properties
  6. Prefer composition - More flexible than inheritance
  7. Use getPrototypeOf - Instead of proto
  8. Document inheritance chains - Help other developers

Understanding prototypes is essential for mastering JavaScript. While classes provide cleaner syntax, knowing prototypes helps you understand what’s happening under the hood.