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!