Mastering JavaScript Prototypes: A Simple Guide

August 19, 2024

When I first started learning JavaScript, the concept of prototypes was quite confusing to me. I felt completely lost as they seemed abstract and difficult to grasp. Even after learning the 'class' syntax, which is essentially a more user-friendly approach to working with prototypes, my understanding didn't improve; it simply made things easier by allowing me to rely on classes. When ChatGPT first became available for beta testing, one of the very first things I asked it to do was to explain JavaScript prototypes in simple terms. That explanation finally unlocked everything, and I gained a better understanding of how prototypes work in JavaScript.

In this post, I’m going to share a straightforward explanation with you. Although you could easily ask for it yourself, I wanted to make it the focus of my first blog post. I will break down JavaScript prototypes in a way that’s easy to understand, even if, like me, you’ve struggled with this concept before.


Unveiling the Prototype: The Blueprint

Imagine a blueprint for building a car. This blueprint defines the car's core features, like having four wheels or an engine. In JavaScript, a prototype serves the same purpose for objects. It acts as a template that other objects can inherit properties and methods from.

Every JavaScript object has an internal link to another object called its [[Prototype]]. These linked objects form a chain called the prototype chain.

Example:

const carPrototype = {
  wheels: 4,
  drive: function () {
    console.log("The car is driving");
  },
};

const myCar = Object.create(carPrototype);
console.log(myCar.wheels); // 4

// Here, carPrototype is the blueprint, and myCar inherits the wheels property and drive method from it.

Navigating the Prototype Chain

When you try to access a property or method on an object, JavaScript follows a specific search path:

  1. Object Itself: First, it checks the object itself to see if the property or method exists directly within it.
  2. Prototype: If not found, JavaScript climbs the prototype chain and checks the object's prototype.
  3. Chain Traversal: This process continues until the property or method is found, or the end of the chain is reached.

Think of it like searching for something in your house. You first look in the room you're in, then maybe the next room, and so on, until you find what you need.

Example:

console.log(myCar.drive); // Found in carPrototype, so it works
console.log(myCar.honk); // Not found anywhere, so it's undefined

// myCar doesn't have its own honk method, so JavaScript looks up the chain and finds it in carPrototype. However, there's no honk method defined anywhere, resulting in undefined.

Crafting Objects with Constructor Functions

JavaScript provides constructor functions as a way to create objects. These functions serve as blueprints for creating new objects with specific properties and methods.

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

const person1 = new Person("John");

Here's the key: When you create an object with a constructor function, the new object's [[Prototype]] is automatically set to the prototype property of that constructor function.

Example:

function Car(model) {
  this.model = model;
}

const myCar = new Car("Toyota");
console.log(myCar.model); // Toyota

// Here, Car is the constructor function, and myCar inherits its prototype when created with new Car('Toyota').

Leveraging the Prototype Property

Every function in JavaScript has a prototype property, which is itself an object. This is where you define properties and methods that will be shared by all objects created with that constructor function.

Example:

Person.prototype.sayHello = function () {
  console.log(`Hello, my name is ${this.name}`);
};

person1.sayHello(); // Outputs: Hello, my name is John

// By adding sayHello to Person.prototype, all Person objects (like person1) can use this method.

Creating Objects with Object.create()

You can bypass constructor functions altogether and create objects directly from a specific prototype using Object.create().

const person2 = Object.create(Person.prototype);
person2.name = "Jane";
person2.sayHello(); // Outputs: Hello, my name is Jane

// Here, person2 is created directly from Person.prototype, inheriting its properties and methods.

Demystifying __proto__ and prototype

  • __proto__: This is an outdated way to access an object's [[Prototype]]. It's recommended to use Object.getPrototypeOf() instead.
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
  • prototype: This is a property of constructor functions that determines the prototype used by objects created with that function.
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // true

Rules of Prototypical Inheritance

To effectively utilize prototypes, keep these rules in mind:

  • No Circular References: An object cannot be its own prototype, directly or indirectly. This prevents infinite loops in the prototype chain.
  • __proto__ Must Be an Object or Null: The prototype of an object must be another object or null.
  • Single Inheritance: JavaScript supports single inheritance, meaning an object can only inherit directly from one other object.

Example:

const obj = {};
obj.__proto__ = obj; // This would create a circular reference, which is not allowed.

Inheriting Getter and Setter Methods

Prototypes can also be used to inherit getter and setter methods, which provide controlled access to object properties.

const Person = {
  get legs() {
    return this._legs || 2;
  },
  set legs(value) {
    this._legs = value;
  },
};

const john = Object.create(Person);
john.legs = 4;
console.log(john.legs); // Outputs: 4