JavaScript Essentials

Prototypes & the Prototype Chain

50 min Lesson 35 of 60

Understanding Prototypes in JavaScript

JavaScript is a prototype-based language. Unlike classical object-oriented languages such as Java or C++ that rely on classes as blueprints, JavaScript uses prototypes as the mechanism for inheritance and shared behavior. Every object in JavaScript has an internal link to another object called its prototype. When you access a property on an object and that property does not exist on the object itself, JavaScript follows the prototype link to search for that property on the prototype object, then on the prototype's prototype, and so on until it reaches the end of the chain. This mechanism is known as the prototype chain, and it is one of the most fundamental concepts in JavaScript.

Understanding prototypes is essential for writing efficient JavaScript code. Prototypes allow objects to share methods and properties without duplicating them in memory. They are the foundation upon which the class syntax introduced in ES6 is built. Mastering prototypes gives you a deep understanding of how JavaScript objects actually work under the hood, which is invaluable for debugging, performance optimization, and designing robust application architectures.

The __proto__ Property vs the prototype Property

One of the most confusing aspects of JavaScript prototypes is the distinction between __proto__ and prototype. These two properties serve different purposes, and mixing them up is a common source of bugs.

The __proto__ property exists on every object. It is the actual reference to the object's prototype -- the object from which it inherits properties and methods. When the JavaScript engine looks up a property on an object and cannot find it, it follows the __proto__ link to continue the search.

The prototype property exists only on functions (specifically, constructor functions and classes). It is the object that will become the __proto__ of any new object created using that function as a constructor with the new keyword.

Example: __proto__ vs prototype

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

Person.prototype.greet = function() {
    return 'Hello, I am ' + this.name;
};

const alice = new Person('Alice');

// alice.__proto__ points to Person.prototype
console.log(alice.__proto__ === Person.prototype); // true

// Person.prototype is an object with the greet method
console.log(alice.greet()); // "Hello, I am Alice"

// alice itself does not have greet -- it comes from the prototype
console.log(alice.hasOwnProperty('greet')); // false
console.log(alice.hasOwnProperty('name'));  // true
Note: The __proto__ property is a legacy feature. While it is widely supported by browsers for compatibility, it is not part of the official ECMAScript specification as a standard property. The recommended way to get or set an object's prototype is through Object.getPrototypeOf() and Object.setPrototypeOf().

Object.getPrototypeOf() and Object.setPrototypeOf()

The modern and recommended way to work with an object's prototype is through the static methods Object.getPrototypeOf() and Object.setPrototypeOf(). These methods provide a clean and standard interface for reading and modifying the prototype chain.

Example: Using Object.getPrototypeOf()

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

Animal.prototype.describe = function() {
    return 'I am a ' + this.type;
};

const dog = new Animal('Dog');

// Get the prototype of dog
const proto = Object.getPrototypeOf(dog);
console.log(proto === Animal.prototype); // true
console.log(proto.describe);             // [Function: describe]

// Check the prototype chain further
const protoOfProto = Object.getPrototypeOf(proto);
console.log(protoOfProto === Object.prototype); // true

// The end of the chain
console.log(Object.getPrototypeOf(protoOfProto)); // null
Warning: Avoid using Object.setPrototypeOf() in performance-critical code. Changing the prototype of an existing object forces the JavaScript engine to abandon optimizations it has made for that object's shape, resulting in significant performance degradation. If you need to set a prototype, prefer doing so at object creation time using Object.create().

Object.create() -- Creating Objects with a Specific Prototype

The Object.create() method creates a new object with a specified prototype. This is the cleanest and most performant way to establish prototype relationships. It allows you to create objects that inherit from any other object without needing a constructor function.

Example: Object.create() Basics

const vehicle = {
    start: function() {
        return this.make + ' is starting...';
    },
    stop: function() {
        return this.make + ' has stopped.';
    }
};

// Create a new object with vehicle as its prototype
const car = Object.create(vehicle);
car.make = 'Toyota';
car.doors = 4;

console.log(car.start()); // "Toyota is starting..."
console.log(car.stop());  // "Toyota has stopped."

// car does not own start or stop -- they come from the prototype
console.log(car.hasOwnProperty('start')); // false
console.log(car.hasOwnProperty('make'));  // true

// Verify the prototype link
console.log(Object.getPrototypeOf(car) === vehicle); // true

Example: Object.create() with Property Descriptors

const base = {
    type: 'base',
    identify: function() {
        return 'Type: ' + this.type;
    }
};

const derived = Object.create(base, {
    type: {
        value: 'derived',
        writable: true,
        enumerable: true,
        configurable: true
    },
    version: {
        value: 2,
        writable: false,
        enumerable: true,
        configurable: false
    }
});

console.log(derived.identify()); // "Type: derived"
console.log(derived.version);    // 2
derived.version = 3;             // Silently fails (or throws in strict mode)
console.log(derived.version);    // 2

Prototype Chain Lookup -- How JavaScript Finds Properties

When you access a property on an object, JavaScript follows a precise algorithm. It first looks at the object itself for the property. If the property is not found, it follows the __proto__ link to the object's prototype and searches there. This process repeats, moving up the chain from one prototype to the next, until the property is found or the chain ends at null. If the property is not found anywhere in the chain, undefined is returned.

Example: Prototype Chain Lookup in Action

const grandparent = {
    family: 'Smith',
    heritage: function() {
        return 'The ' + this.family + ' family';
    }
};

const parent = Object.create(grandparent);
parent.job = 'Engineer';

const child = Object.create(parent);
child.name = 'Alice';

// Property lookup chain:
// 1. child.name -- found on child itself
console.log(child.name); // "Alice"

// 2. child.job -- not on child, found on parent (child.__proto__)
console.log(child.job); // "Engineer"

// 3. child.family -- not on child, not on parent,
//    found on grandparent (child.__proto__.__proto__)
console.log(child.family); // "Smith"

// 4. child.heritage() -- method found on grandparent
console.log(child.heritage()); // "The Smith family"

// 5. child.age -- not found anywhere in the chain
console.log(child.age); // undefined

Property Shadowing

Property shadowing occurs when an object has a property with the same name as a property on its prototype. The object's own property "shadows" or hides the prototype's property. When you access that property, you get the object's own version, not the prototype's version. The prototype's property still exists and is accessible through other objects that share that prototype.

Example: Property Shadowing

const defaults = {
    color: 'blue',
    size: 'medium',
    getInfo: function() {
        return this.color + ' / ' + this.size;
    }
};

const custom = Object.create(defaults);
custom.color = 'red'; // Shadows defaults.color

console.log(custom.color);     // "red" (own property)
console.log(defaults.color);   // "blue" (unchanged)
console.log(custom.size);      // "medium" (inherited)
console.log(custom.getInfo()); // "red / medium"

// Deleting the own property reveals the prototype property
delete custom.color;
console.log(custom.color);     // "blue" (now inherited again)
console.log(custom.getInfo()); // "blue / medium"
Pro Tip: Property shadowing is the mechanism behind method overriding in JavaScript. When a subclass defines a method with the same name as a method on the superclass prototype, the subclass version shadows the superclass version. You can still access the original method using Object.getPrototypeOf(obj).method.call(obj) or through super.method() in class syntax.

The constructor Property

Every function's prototype object has a constructor property that points back to the function itself. This creates a circular reference: the function points to its prototype, and the prototype points back to the function. This property is used to identify which constructor function created an object.

Example: The constructor Property

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

const myCar = new Car('Honda', 'Civic');

// constructor points back to the Car function
console.log(myCar.constructor === Car); // true

// You can use constructor to create new instances
const anotherCar = new myCar.constructor('Ford', 'Focus');
console.log(anotherCar.make);  // "Ford"
console.log(anotherCar.model); // "Focus"

// Be careful when replacing prototype entirely
Car.prototype = {
    drive: function() { return 'Driving...'; }
};

const newCar = new Car('BMW', 'X5');
console.log(newCar.constructor === Car);    // false!
console.log(newCar.constructor === Object); // true (inherited from Object.prototype)

// Always restore constructor when replacing prototype
Car.prototype = {
    constructor: Car, // Restore the constructor reference
    drive: function() { return 'Driving...'; }
};

const fixedCar = new Car('Audi', 'A4');
console.log(fixedCar.constructor === Car); // true

Prototypal Inheritance Patterns

Before ES6 classes, developers used several patterns to implement inheritance in JavaScript. Understanding these patterns is important because they reveal how JavaScript inheritance actually works, and the class syntax is syntactic sugar over these same patterns.

Example: Constructor Pattern with Prototype Chain

// Parent constructor
function Shape(color) {
    this.color = color;
}

Shape.prototype.describe = function() {
    return 'A ' + this.color + ' shape';
};

// Child constructor
function Circle(color, radius) {
    Shape.call(this, color); // Call parent constructor
    this.radius = radius;
}

// Set up inheritance -- Circle.prototype inherits from Shape.prototype
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle; // Restore constructor

Circle.prototype.area = function() {
    return Math.PI * this.radius * this.radius;
};

Circle.prototype.describe = function() {
    // Call parent method and extend it
    const parentDesc = Shape.prototype.describe.call(this);
    return parentDesc + ' (circle, radius: ' + this.radius + ')';
};

const c = new Circle('red', 5);
console.log(c.describe());         // "A red shape (circle, radius: 5)"
console.log(c.area().toFixed(2));   // "78.54"
console.log(c instanceof Circle);  // true
console.log(c instanceof Shape);   // true

Example: Pure Object-Based Inheritance (No Constructors)

const eventEmitter = {
    _listeners: null,

    on: function(event, callback) {
        if (!this._listeners) this._listeners = {};
        if (!this._listeners[event]) this._listeners[event] = [];
        this._listeners[event].push(callback);
    },

    emit: function(event, data) {
        if (!this._listeners || !this._listeners[event]) return;
        this._listeners[event].forEach(function(cb) {
            cb(data);
        });
    }
};

const logger = Object.create(eventEmitter);
logger.log = function(message) {
    console.log('[LOG] ' + message);
    this.emit('logged', { message: message });
};

logger.on('logged', function(data) {
    console.log('Event fired: ' + data.message);
});

logger.log('Application started');
// [LOG] Application started
// Event fired: Application started

Object.prototype Methods

Object.prototype sits at the top of the prototype chain for almost every object in JavaScript. It provides several methods that are available on all objects. Understanding these methods is important because they are often overridden to customize object behavior.

hasOwnProperty()

The hasOwnProperty() method returns true if the object has the specified property as its own (non-inherited) property. This is critical for distinguishing between own properties and inherited ones.

Example: hasOwnProperty() for Safe Property Checking

const config = Object.create({ defaultTimeout: 3000 });
config.apiUrl = 'https://api.example.com';
config.retries = 3;

// Iterate only over own properties
for (const key in config) {
    if (config.hasOwnProperty(key)) {
        console.log(key + ': ' + config[key]);
    }
}
// Output:
// apiUrl: https://api.example.com
// retries: 3
// (defaultTimeout is skipped because it is inherited)

// Modern alternative: Object.hasOwn() (ES2022)
for (const key in config) {
    if (Object.hasOwn(config, key)) {
        console.log(key + ': ' + config[key]);
    }
}

toString() and valueOf()

The toString() method returns a string representation of the object, and valueOf() returns the primitive value of the object. These methods are called automatically by JavaScript in certain contexts, such as string concatenation or arithmetic operations.

Example: Overriding toString() and valueOf()

function Money(amount, currency) {
    this.amount = amount;
    this.currency = currency;
}

Money.prototype.toString = function() {
    return this.amount.toFixed(2) + ' ' + this.currency;
};

Money.prototype.valueOf = function() {
    return this.amount;
};

const price = new Money(29.99, 'USD');

// toString() is called in string context
console.log('Price: ' + price);        // "Price: 29.99 USD"
console.log(String(price));             // "29.99 USD"
console.log(`Item costs ${price}`);     // "Item costs 29.99 USD"

// valueOf() is called in numeric context
console.log(price + 10);    // 39.99
console.log(price > 20);    // true
console.log(price * 2);     // 59.98

Extending Built-in Prototypes (and Why Not To)

Because JavaScript allows you to modify any prototype, you can add methods to built-in objects like Array.prototype, String.prototype, or even Object.prototype. While this is technically possible, it is considered a very bad practice for several important reasons.

Example: Extending Built-in Prototypes (Dangerous!)

// Adding a method to Array.prototype -- DO NOT DO THIS
Array.prototype.last = function() {
    return this[this.length - 1];
};

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

// Why this is dangerous:

// 1. Name collisions -- a future JavaScript standard might add
//    Array.prototype.last with different behavior
// 2. Breaks for...in loops on arrays
const arr = [10, 20, 30];
for (const key in arr) {
    console.log(key); // "0", "1", "2", "last" -- unexpected!
}

// 3. Affects ALL arrays in the entire application
// 4. Third-party libraries might add the same method differently
// 5. Makes debugging extremely difficult

// Clean up
delete Array.prototype.last;
Warning: Never modify built-in prototypes in production code. If you need custom utility methods, create standalone functions or use a utility class instead. The only widely accepted exception is polyfilling -- adding standardized methods that are missing in older environments, and even then you should check if the method already exists before adding it.

Example: Safe Polyfill Pattern

// Safe polyfill -- only add if the method does not exist
if (!Array.prototype.at) {
    Array.prototype.at = function(index) {
        if (index < 0) {
            return this[this.length + index];
        }
        return this[index];
    };
}

// Better alternative: standalone utility functions
function lastElement(arr) {
    return arr[arr.length - 1];
}

console.log(lastElement([1, 2, 3])); // 3

Class Inheritance Under the Hood

ES6 introduced the class syntax, which provides a cleaner way to define constructors and set up prototype chains. However, classes in JavaScript are not a new inheritance model -- they are syntactic sugar over the existing prototypal inheritance system. Understanding what happens under the hood helps you debug issues and write more effective code.

Example: Class Syntax vs Prototype Equivalent

// ES6 Class Syntax
class Animal {
    constructor(name, sound) {
        this.name = name;
        this.sound = sound;
    }

    speak() {
        return this.name + ' says ' + this.sound;
    }

    static create(name, sound) {
        return new Animal(name, sound);
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name, 'Woof');
        this.tricks = [];
    }

    learn(trick) {
        this.tricks.push(trick);
    }

    showTricks() {
        return this.name + ' knows: ' + this.tricks.join(', ');
    }
}

// What the engine actually does (simplified):
// function Animal(name, sound) {
//     this.name = name;
//     this.sound = sound;
// }
// Animal.prototype.speak = function() {
//     return this.name + ' says ' + this.sound;
// };
// Animal.create = function(name, sound) {
//     return new Animal(name, sound);
// };
//
// function Dog(name) {
//     Animal.call(this, name, 'Woof');
//     this.tricks = [];
// }
// Dog.prototype = Object.create(Animal.prototype);
// Dog.prototype.constructor = Dog;
// Object.setPrototypeOf(Dog, Animal); // For static method inheritance

const rex = new Dog('Rex');
rex.learn('sit');
rex.learn('shake');

console.log(rex.speak());      // "Rex says Woof"
console.log(rex.showTricks()); // "Rex knows: sit, shake"

// Proving it is all prototypes under the hood
console.log(typeof Animal);                          // "function"
console.log(rex.__proto__ === Dog.prototype);        // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

Visualizing the Prototype Chain

Understanding the prototype chain becomes much easier when you visualize it as a linked chain of objects. Every chain ultimately ends at Object.prototype, whose own prototype is null.

Example: Tracing the Full Prototype Chain

function tracePrototypeChain(obj) {
    const chain = [];
    let current = obj;

    while (current !== null) {
        if (current === obj) {
            chain.push('[instance: ' + (current.constructor?.name || 'Object') + ']');
        } else if (current.constructor) {
            chain.push(current.constructor.name + '.prototype');
        } else {
            chain.push('[null prototype]');
        }
        current = Object.getPrototypeOf(current);
    }
    chain.push('null');
    return chain.join(' --> ');
}

// Regular object
const obj = { x: 1 };
console.log(tracePrototypeChain(obj));
// [instance: Object] --> Object.prototype --> null

// Array
const arr = [1, 2, 3];
console.log(tracePrototypeChain(arr));
// [instance: Array] --> Array.prototype --> Object.prototype --> null

// Custom inheritance chain
function Base() {}
function Middle() {}
Middle.prototype = Object.create(Base.prototype);
Middle.prototype.constructor = Middle;
function Leaf() {}
Leaf.prototype = Object.create(Middle.prototype);
Leaf.prototype.constructor = Leaf;

const instance = new Leaf();
console.log(tracePrototypeChain(instance));
// [instance: Leaf] --> Leaf.prototype --> Middle.prototype --> Base.prototype --> Object.prototype --> null

// Object with null prototype (no chain)
const bare = Object.create(null);
console.log(Object.getPrototypeOf(bare)); // null
// bare has NO inherited methods -- no toString, no hasOwnProperty
Note: Objects created with Object.create(null) have no prototype chain at all. They are sometimes called "dictionary objects" or "bare objects" because they have zero inherited properties. This makes them useful as pure key-value stores where you do not want inherited properties like toString or hasOwnProperty to interfere with your data.

Performance Implications of Prototypes

The prototype chain has direct implications for performance. Understanding these implications helps you write faster code and avoid common performance pitfalls.

Property Lookup Cost: Every time you access a property, the engine may need to traverse the prototype chain. Longer chains mean more lookups. Modern JavaScript engines optimize this with inline caches that remember where a property was found, but modifying prototypes at runtime invalidates these caches.

Memory Efficiency: Methods defined on the prototype are shared across all instances. This is far more memory-efficient than defining methods inside the constructor, which creates a new function object for every instance.

Example: Memory Efficiency -- Prototype vs Constructor Methods

// BAD: Methods in constructor (new function per instance)
function IneffcientUser(name) {
    this.name = name;
    this.greet = function() { // New function created for EVERY instance
        return 'Hello, ' + this.name;
    };
}

// GOOD: Methods on prototype (shared across all instances)
function EfficientUser(name) {
    this.name = name;
}
EfficientUser.prototype.greet = function() { // One function shared by all
    return 'Hello, ' + this.name;
};

const users1 = [];
const users2 = [];

for (let i = 0; i < 1000; i++) {
    users1.push(new IneffcientUser('User' + i));
    users2.push(new EfficientUser('User' + i));
}

// users1: 1000 separate greet functions in memory
// users2: 1 shared greet function on the prototype

// Verify they are different function objects (inefficient)
console.log(users1[0].greet === users1[1].greet); // false

// Verify they share the same function object (efficient)
console.log(users2[0].greet === users2[1].greet); // true

Example: Performance Impact of Prototype Chain Depth

// Shallow chain -- fast lookups
const shallow = Object.create({ sharedMethod: function() { return 42; } });

// Deep chain -- slower lookups for deeply inherited properties
let deep = { deepMethod: function() { return 42; } };
for (let i = 0; i < 20; i++) {
    deep = Object.create(deep);
}

// Benchmark: accessing a method on shallow vs deep chain
console.time('shallow');
for (let i = 0; i < 1000000; i++) {
    shallow.sharedMethod();
}
console.timeEnd('shallow');

console.time('deep');
for (let i = 0; i < 1000000; i++) {
    deep.deepMethod();
}
console.timeEnd('deep');

// Tip: cache deeply inherited properties in local variables
// when accessing them in tight loops
const cachedMethod = deep.deepMethod;
console.time('cached');
for (let i = 0; i < 1000000; i++) {
    cachedMethod();
}
console.timeEnd('cached');
Pro Tip: Keep your prototype chains shallow. In practice, a chain depth of two to three levels is common and works well. Extremely deep chains (more than five to six levels) can impact property lookup performance, especially in hot code paths. If you need deeply nested inheritance hierarchies, consider composition over inheritance as an alternative pattern.

Practical Patterns and Best Practices

Now that you understand the mechanics of prototypes, here are some practical patterns and best practices for using them effectively in real applications.

Example: Mixin Pattern Using Object.assign()

// Mixins allow you to compose behavior from multiple sources
const serializable = {
    toJSON: function() {
        const result = {};
        for (const key in this) {
            if (this.hasOwnProperty(key) && typeof this[key] !== 'function') {
                result[key] = this[key];
            }
        }
        return JSON.stringify(result);
    }
};

const validatable = {
    validate: function() {
        for (const rule of this._validationRules || []) {
            if (!rule.check(this[rule.field])) {
                return { valid: false, error: rule.message };
            }
        }
        return { valid: true };
    }
};

function Product(name, price) {
    this.name = name;
    this.price = price;
    this._validationRules = [
        { field: 'name', check: function(v) { return v && v.length > 0; }, message: 'Name is required' },
        { field: 'price', check: function(v) { return v > 0; }, message: 'Price must be positive' }
    ];
}

// Mix in capabilities
Object.assign(Product.prototype, serializable, validatable);

const item = new Product('Widget', 9.99);
console.log(item.toJSON());    // '{"name":"Widget","price":9.99}'
console.log(item.validate());  // { valid: true }

Example: Checking Prototype Relationships

function Shape() {}
function Rect() {}
Rect.prototype = Object.create(Shape.prototype);
Rect.prototype.constructor = Rect;

const r = new Rect();

// instanceof checks the entire prototype chain
console.log(r instanceof Rect);   // true
console.log(r instanceof Shape);  // true
console.log(r instanceof Object); // true

// isPrototypeOf checks if an object exists in another's chain
console.log(Shape.prototype.isPrototypeOf(r)); // true
console.log(Rect.prototype.isPrototypeOf(r));  // true
console.log(Array.prototype.isPrototypeOf(r)); // false

Practice Exercise

Create an inheritance hierarchy for a library management system. Define a base LibraryItem constructor that takes title and year as parameters and stores them as instance properties. Add a getSummary() method to LibraryItem.prototype that returns a string in the format "Title (Year)". Then create a Book constructor that extends LibraryItem and adds an author property and a pageCount property. Override getSummary() on Book.prototype to include the author name. Create a DVD constructor that extends LibraryItem and adds a director property and a duration property. Override getSummary() on DVD.prototype to include the director and duration. Write a traceChain() function that takes any object and returns an array of constructor names in its prototype chain. Test with instances of both Book and DVD. Verify that instanceof works correctly for all three constructors. Finally, create a mixin called borrowable that adds checkOut() and checkIn() methods, and mix it into both Book.prototype and DVD.prototype.