Advanced JavaScript (ES6+)

Prototypes and Prototype Chain

13 min Lesson 28 of 40

Prototypes and Prototype Chain

Understanding prototypes is crucial to mastering JavaScript. Every JavaScript object has a prototype, which is a mechanism for inheritance. Let's dive deep into how prototypes work and how they power JavaScript's inheritance model.

What is a Prototype?

A prototype is an object from which other objects inherit properties and methods. In JavaScript, every object has an internal link to another object called its prototype.

Key Concept: JavaScript uses prototypal inheritance, not classical inheritance like Java or C++. ES6 classes are syntactic sugar over this prototype-based system.

Understanding __proto__ vs prototype

There are two important properties to understand:

// Every object has __proto__ (link to its prototype) const obj = {}; console.log(obj.__proto__); // Object.prototype // Constructor functions have a prototype property function Person(name) { this.name = name; } Person.prototype.greet = function() { return `Hello, I'm ${this.name}`; }; const john = new Person("John"); // __proto__ points to the constructor's prototype console.log(john.__proto__ === Person.prototype); // true // The prototype has a constructor property pointing back console.log(Person.prototype.constructor === Person); // true
Key Differences: __proto__: - Property on every object - Points to the object's prototype - Used for prototype chain lookup prototype: - Property on constructor functions and classes - Defines properties/methods for instances - Used when creating new objects with "new"
Important: While __proto__ works in browsers, it's deprecated. Use Object.getPrototypeOf() and Object.setPrototypeOf() instead for production code.

The Prototype Chain

When you access a property on an object, JavaScript first looks for it on the object itself. If not found, it looks on the object's prototype, then the prototype's prototype, and so on:

function Animal(name) { this.name = name; } Animal.prototype.eat = function() { return `${this.name} is eating`; }; function Dog(name, breed) { Animal.call(this, name); this.breed = breed; } // Set up inheritance Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { return `${this.name} says Woof!`; }; const myDog = new Dog("Max", "Labrador"); console.log(myDog.bark()); // "Max says Woof!" (found on Dog.prototype) console.log(myDog.eat()); // "Max is eating" (found on Animal.prototype) console.log(myDog.toString()); // "[object Object]" (found on Object.prototype) // Prototype chain visualization: // myDog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null

Checking the Prototype Chain

Several methods help you inspect the prototype chain:

function Person(name) { this.name = name; } Person.prototype.greet = function() { return `Hello, I'm ${this.name}`; }; const john = new Person("John"); // Get the prototype console.log(Object.getPrototypeOf(john) === Person.prototype); // true // Check if property exists on object (not prototype) console.log(john.hasOwnProperty("name")); // true console.log(john.hasOwnProperty("greet")); // false (it's on prototype) // Check if property exists anywhere in chain console.log("name" in john); // true console.log("greet" in john); // true console.log("toString" in john); // true (from Object.prototype) // Check if object is in prototype chain console.log(Person.prototype.isPrototypeOf(john)); // true console.log(Object.prototype.isPrototypeOf(john)); // true console.log(Array.prototype.isPrototypeOf(john)); // false

Object.create() for Prototypal Inheritance

Object.create() creates a new object with a specified prototype:

// Create a prototype object const personPrototype = { greet() { return `Hello, I'm ${this.name}`; }, introduce() { return `My name is ${this.name} and I'm ${this.age} years old`; } }; // Create objects with this prototype const john = Object.create(personPrototype); john.name = "John"; john.age = 30; const sarah = Object.create(personPrototype); sarah.name = "Sarah"; sarah.age = 25; console.log(john.greet()); // "Hello, I'm John" console.log(sarah.introduce()); // "My name is Sarah and I'm 25 years old" // Both share the same prototype console.log(Object.getPrototypeOf(john) === Object.getPrototypeOf(sarah)); // true

Prototype Chain with Classes

ES6 classes also use the prototype chain under the hood:

class Animal { constructor(name) { this.name = name; } eat() { return `${this.name} is eating`; } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } bark() { return `${this.name} says Woof!`; } } const myDog = new Dog("Max", "Labrador"); // Check the prototype chain console.log(myDog.__proto__ === Dog.prototype); // true console.log(Dog.prototype.__proto__ === Animal.prototype); // true console.log(Animal.prototype.__proto__ === Object.prototype); // true console.log(Object.prototype.__proto__ === null); // true (end of chain) // The prototype chain: // myDog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null

Modifying Prototypes

You can add or modify prototype methods even after objects are created:

function Calculator() { this.result = 0; } Calculator.prototype.add = function(num) { this.result += num; return this; }; const calc1 = new Calculator(); const calc2 = new Calculator(); calc1.add(5).add(3); console.log(calc1.result); // 8 // Add a new method to the prototype Calculator.prototype.multiply = function(num) { this.result *= num; return this; }; // Both existing and new instances get the method calc1.multiply(2); console.log(calc1.result); // 16 calc2.add(10).multiply(3); console.log(calc2.result); // 30
Best Practice: While you can modify prototypes at runtime, it's generally not recommended as it can lead to unexpected behavior and performance issues. Define all methods upfront.

Built-in Prototypes

JavaScript's built-in objects also use prototypes:

// Array prototype const arr = [1, 2, 3]; console.log(arr.__proto__ === Array.prototype); // true console.log(Array.prototype.__proto__ === Object.prototype); // true // Array methods come from the prototype console.log(arr.hasOwnProperty("push")); // false console.log("push" in arr); // true console.log(Array.prototype.hasOwnProperty("push")); // true // String prototype const str = "hello"; console.log(str.__proto__ === String.prototype); // true // Number prototype const num = 42; console.log(num.__proto__ === Number.prototype); // true // Function prototype (yes, functions are objects!) function myFunc() {} console.log(myFunc.__proto__ === Function.prototype); // true

Extending Built-in Prototypes (Be Careful!)

You can extend built-in prototypes, but it's generally discouraged:

// Adding a method to Array prototype Array.prototype.last = function() { return this[this.length - 1]; }; const numbers = [1, 2, 3, 4, 5]; console.log(numbers.last()); // 5 // Adding to String prototype String.prototype.reverse = function() { return this.split("").reverse().join(""); }; console.log("hello".reverse()); // "olleh"
Caution: Extending built-in prototypes can cause conflicts with future JavaScript features or third-party libraries. Only do this if absolutely necessary and ensure unique method names.

Property Shadowing

When you set a property on an object, it shadows the prototype's property:

const parent = { name: "Parent", greet() { return `Hello from ${this.name}`; } }; const child = Object.create(parent); console.log(child.name); // "Parent" (from prototype) console.log(child.greet()); // "Hello from Parent" // Shadow the name property child.name = "Child"; console.log(child.name); // "Child" (own property) console.log(child.greet()); // "Hello from Child" (uses own property) // Check properties console.log(child.hasOwnProperty("name")); // true console.log(child.hasOwnProperty("greet")); // false // Delete own property to reveal prototype property delete child.name; console.log(child.name); // "Parent" (from prototype again)

Modern Alternatives to Prototypes

Modern JavaScript offers cleaner alternatives for most use cases:

// Old way: Constructor + Prototype function PersonOld(name, age) { this.name = name; this.age = age; } PersonOld.prototype.greet = function() { return `Hello, I'm ${this.name}`; }; // Modern way: ES6 Class class PersonNew { constructor(name, age) { this.name = name; this.age = age; } greet() { return `Hello, I'm ${this.name}`; } } // Modern way: Object with methods const createPerson = (name, age) => ({ name, age, greet() { return `Hello, I'm ${this.name}`; } }); // All three approaches work, but classes are preferred const person1 = new PersonOld("John", 30); const person2 = new PersonNew("Jane", 25); const person3 = createPerson("Bob", 35);

Performance Considerations

Understanding prototypes helps with performance optimization:

// Good: Methods on prototype (shared by all instances) class GoodPerson { constructor(name) { this.name = name; } greet() { return `Hello, I'm ${this.name}`; } } const p1 = new GoodPerson("John"); const p2 = new GoodPerson("Jane"); // Both share the same greet method // Bad: Methods defined in constructor (separate copy for each instance) class BadPerson { constructor(name) { this.name = name; this.greet = function() { return `Hello, I'm ${this.name}`; }; } } const p3 = new BadPerson("John"); const p4 = new BadPerson("Jane"); // Each has its own greet method (wastes memory) console.log(p1.greet === p2.greet); // true (same reference) console.log(p3.greet === p4.greet); // false (different references)
Performance Tip: Methods defined on the prototype are shared by all instances, saving memory. Methods defined in the constructor create a new copy for each instance.

Practice Exercise:

Challenge: Create a prototype-based implementation of a simple linked list with the following requirements:

  • Node constructor with value and next properties
  • LinkedList constructor
  • Prototype methods: append(value), prepend(value), find(value), size()
  • Test your implementation

Solution:

// Node constructor function Node(value) { this.value = value; this.next = null; } // LinkedList constructor function LinkedList() { this.head = null; } // Add methods to prototype LinkedList.prototype.append = function(value) { const newNode = new Node(value); if (!this.head) { this.head = newNode; return; } let current = this.head; while (current.next) { current = current.next; } current.next = newNode; }; LinkedList.prototype.prepend = function(value) { const newNode = new Node(value); newNode.next = this.head; this.head = newNode; }; LinkedList.prototype.find = function(value) { let current = this.head; while (current) { if (current.value === value) { return true; } current = current.next; } return false; }; LinkedList.prototype.size = function() { let count = 0; let current = this.head; while (current) { count++; current = current.next; } return count; }; LinkedList.prototype.toArray = function() { const result = []; let current = this.head; while (current) { result.push(current.value); current = current.next; } return result; }; // Test the implementation const list = new LinkedList(); list.append(1); list.append(2); list.append(3); list.prepend(0); console.log(list.toArray()); // [0, 1, 2, 3] console.log(list.size()); // 4 console.log(list.find(2)); // true console.log(list.find(5)); // false // Check prototype console.log(list.hasOwnProperty("append")); // false (on prototype) console.log(list.hasOwnProperty("head")); // true (own property)

Summary

In this lesson, you learned:

  • Prototypes are the mechanism for inheritance in JavaScript
  • __proto__ links objects to their prototype
  • prototype property defines methods for constructor-created objects
  • The prototype chain allows property lookup through multiple levels
  • Object.create() creates objects with specified prototypes
  • ES6 classes use prototypes under the hood
  • Methods on prototypes are shared, saving memory
  • Modern classes provide cleaner syntax than manual prototype manipulation
Next Up: In the next lesson, we'll explore powerful object methods including Object.keys(), Object.values(), Object.entries(), and more!