Prototypes & the Prototype Chain
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
__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
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"
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;
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
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');
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.