Skip to main content

JavaScript Object Prototypes

Introduction

In JavaScript, every object has a hidden property called prototype that links to another object. This connection creates what's known as the prototype chain, a fundamental concept in JavaScript's object-oriented programming model. Understanding prototypes is crucial because it reveals how JavaScript implements inheritance—a powerful way to share properties and methods between objects.

Unlike traditional class-based languages, JavaScript uses a prototype-based inheritance mechanism that might seem unusual at first, but once mastered, gives you tremendous flexibility in creating and extending objects.

What Are Prototypes?

At its core, a prototype is simply an object that serves as a template for other objects. When you create an object in JavaScript, it automatically receives a link to a prototype object.

Let's see a basic example:

javascript
let person = {
name: "John",
age: 30,
greet: function() {
return `Hello, my name is ${this.name}`;
}
};

console.log(person.greet()); // Output: Hello, my name is John

In this example, person is linked to Object.prototype by default. This means person can access any properties or methods defined on Object.prototype, such as toString() and hasOwnProperty().

The Prototype Chain

When you try to access a property or method on an object, JavaScript follows these steps:

  1. First, it checks if the object itself has that property
  2. If not, it checks the object's prototype
  3. If still not found, it checks the prototype's prototype
  4. This continues until either:
    • The property is found, or
    • The end of the chain is reached (typically Object.prototype, which has null as its prototype)

This sequence of linked prototype objects forms the prototype chain.

javascript
let animal = {
eats: true,
sleep: function() {
return "Sleeping...";
}
};

let rabbit = {
jumps: true,
__proto__: animal // Set rabbit's prototype to animal
};

console.log(rabbit.eats); // Output: true (inherited from animal)
console.log(rabbit.jumps); // Output: true (own property)
console.log(rabbit.sleep()); // Output: Sleeping... (method from animal)
caution

The __proto__ syntax shown above is deprecated for direct code manipulation but is shown here for illustration. Use Object.create(), Object.setPrototypeOf(), or constructor functions instead.

Constructor Functions and Prototypes

A more common way to work with prototypes is through constructor functions:

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

// Adding a method to the prototype
Person.prototype.greet = function() {
return `Hello, my name is ${this.name} and I am ${this.age} years old`;
};

// Creating instances
let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);

console.log(person1.greet()); // Output: Hello, my name is Alice and I am 25 years old
console.log(person2.greet()); // Output: Hello, my name is Bob and I am 30 years old

In this example:

  1. Person is a constructor function
  2. Person.prototype is the object that will become the prototype for all instances created with new Person()
  3. Both person1 and person2 inherit the greet method from Person.prototype

This approach is memory-efficient because the greet method exists in only one place (the prototype) rather than being duplicated in each instance.

Prototype Methods vs. Instance Methods

It's important to understand the difference between adding methods to the prototype versus directly to an instance:

javascript
function Dog(name) {
this.name = name;

// Instance method - created for each instance
this.bark1 = function() {
return `${this.name} says woof!`;
};
}

// Prototype method - shared by all instances
Dog.prototype.bark2 = function() {
return `${this.name} says woof!`;
};

let dog1 = new Dog("Rex");
let dog2 = new Dog("Buddy");

console.log(dog1.bark1 === dog2.bark1); // Output: false (different functions)
console.log(dog1.bark2 === dog2.bark2); // Output: true (same function)

The prototype method (bark2) is more memory-efficient because all instances share the same function.

Checking Prototype Relationships

JavaScript provides several ways to check prototype relationships:

javascript
function Animal() {}
let cat = new Animal();

// Check if an object is an instance of a constructor
console.log(cat instanceof Animal); // Output: true

// Check if an object exists in another object's prototype chain
console.log(Animal.prototype.isPrototypeOf(cat)); // Output: true

// Get an object's prototype
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // Output: true

Extending Built-in Objects Using Prototypes

JavaScript allows you to extend built-in objects like Array or String by adding methods to their prototypes:

javascript
// Adding a method to all arrays
Array.prototype.first = function() {
return this[0];
};

let numbers = [1, 2, 3, 4, 5];
console.log(numbers.first()); // Output: 1
warning

Extending built-in prototypes is generally discouraged in production code as it can lead to naming conflicts, especially when working with multiple libraries. Use with caution!

Real-World Example: Building a Simple Inheritance System

Let's create a more comprehensive example showing inheritance between different types of vehicles:

javascript
// Base Vehicle constructor
function Vehicle(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.isRunning = false;
}

// Adding methods to Vehicle prototype
Vehicle.prototype.startEngine = function() {
this.isRunning = true;
return `${this.make} ${this.model}'s engine is now running`;
};

Vehicle.prototype.stopEngine = function() {
this.isRunning = false;
return `${this.make} ${this.model}'s engine is now off`;
};

Vehicle.prototype.getInfo = function() {
return `${this.year} ${this.make} ${this.model}`;
};

// Car constructor inherits from Vehicle
function Car(make, model, year, numDoors) {
// Call the parent constructor
Vehicle.call(this, make, model, year);
this.numDoors = numDoors;
}

// Set up inheritance
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

// Add Car-specific methods
Car.prototype.honk = function() {
return "Beep beep!";
};

// Motorcycle constructor also inherits from Vehicle
function Motorcycle(make, model, year, hasSidecar) {
Vehicle.call(this, make, model, year);
this.hasSidecar = hasSidecar;
}

// Set up inheritance for Motorcycle
Motorcycle.prototype = Object.create(Vehicle.prototype);
Motorcycle.prototype.constructor = Motorcycle;

// Add Motorcycle-specific methods
Motorcycle.prototype.wheelie = function() {
return this.hasSidecar ? "Cannot do a wheelie with a sidecar!" : "Doing a wheelie!";
};

// Create instances
let sedan = new Car("Honda", "Accord", 2020, 4);
let bike = new Motorcycle("Harley-Davidson", "Sportster", 2019, false);

// Test the inheritance structure
console.log(sedan.getInfo()); // Output: 2020 Honda Accord
console.log(sedan.startEngine()); // Output: Honda Accord's engine is now running
console.log(sedan.honk()); // Output: Beep beep!

console.log(bike.getInfo()); // Output: 2019 Harley-Davidson Sportster
console.log(bike.startEngine()); // Output: Harley-Davidson Sportster's engine is now running
console.log(bike.wheelie()); // Output: Doing a wheelie!

// Verify inheritance relationships
console.log(sedan instanceof Car); // Output: true
console.log(sedan instanceof Vehicle); // Output: true
console.log(bike instanceof Vehicle); // Output: true

This example demonstrates a common pattern for creating inheritance hierarchies in JavaScript using prototypes. We have a base Vehicle object with common functionality, and specific vehicle types (Car and Motorcycle) that inherit and extend this functionality.

Modern JavaScript Prototypes

In modern JavaScript (ES6+), the class syntax provides a cleaner way to work with prototypes:

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

speak() {
return `${this.name} makes a noise.`;
}
}

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

speak() {
return `${this.name} barks!`;
}
}

let dog = new Dog("Rex", "German Shepherd");
console.log(dog.speak()); // Output: Rex barks!

Under the hood, this still uses JavaScript's prototype system, but with a more familiar syntax for developers coming from class-based languages.

Summary

JavaScript's prototype system is a powerful feature that enables:

  1. Inheritance - Objects can inherit properties and methods from other objects
  2. Method Sharing - Multiple objects can share methods without duplication
  3. Dynamic Extension - Prototypes can be modified at runtime, affecting all linked objects

Understanding prototypes is essential for JavaScript developers because it helps you:

  • Create more memory-efficient code
  • Implement inheritance patterns
  • Understand how built-in JavaScript objects work
  • Make sense of frameworks and libraries that leverage prototypes

While modern JavaScript offers the class syntax as syntactic sugar over prototypes, knowing how the underlying prototype mechanism works gives you a deeper understanding of JavaScript's object-oriented nature.

Exercises

  1. Create a Shape constructor with properties for color and name, plus a describe() method. Then create Circle and Square constructors that inherit from Shape and add their own specific properties and methods.

  2. Extend the built-in String prototype with a capitalize() method that returns the string with the first letter capitalized.

  3. Implement a "mixin" pattern where you have a set of methods in an object that can be added to any constructor's prototype.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)