Prototypes and Prototype Chains: The Underlying Mechanism of JavaScript Object Inheritance
Imagine you're using a toolbox and realize you're missing a tool. You'll first check inside your own toolbox, and if you don't find it, you'll borrow from a neighbor or the tool room. JavaScript's object system works in a similar way—every object has its own toolbox, and when it needs a property or method, it first looks in its own toolbox, then searches up the "neighborhood chain" until it finds what it needs or reaches the end of the chain.
This clever mechanism is the core feature of JavaScript: prototypes and prototype chains.
What is a Prototype: An Object's Family Tree
In JavaScript, every object has a hidden internal property [[Prototype]] (accessible via __proto__ in browser environments) that points to another object. This pointed-to object is the current object's prototype.
// Create a simple object
const person = {
name: "Sarah",
age: 29,
introduce() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old`);
},
};
// Check the object's prototype reference
console.log(person.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true - the end of the prototype chainEven this simple person object inherits many built-in methods through the prototype chain:
// The person object doesn't have its own toString method
console.log("toString" in person); // true
console.log(person.hasOwnProperty("toString")); // false
// But we can call it normally because it's on the prototype chain
console.log(person.toString()); // "[object Object]"
// The hasOwnProperty method is also inherited
console.log(person.hasOwnProperty("name")); // true - person has its own name property
console.log(person.hasOwnProperty("toString")); // false - toString is on the prototypeprototype vs proto: The Twins of Functions and Objects
Understanding JavaScript's prototype system requires distinguishing between the prototype and __proto__ concepts. Although their names are similar, their purposes are completely different.
The prototype Property: A Function's Design Blueprint
prototype is a property unique to constructor functions that defines the prototype for objects created by that function.
function Vehicle(brand) {
this.brand = brand;
this.speed = 0;
}
// Vehicle.prototype is the prototype for all Vehicle instances
console.log(Vehicle.prototype); // { constructor: f Vehicle() }
// Add methods to the prototype that all instances will inherit
Vehicle.prototype.start = function () {
console.log(`${this.brand} vehicle is starting...`);
this.speed = 10;
};
Vehicle.prototype.accelerate = function (amount) {
this.speed += amount;
console.log(`${this.brand} is accelerating to ${this.speed} km/h`);
};
// Create instances
const car = new Vehicle("Toyota");
// car's prototype points to Vehicle.prototype
console.log(car.__proto__ === Vehicle.prototype); // trueThe proto Property: An Object's Genealogy Chain
__proto__ is a property unique to objects that points to the object's prototype.
function Vehicle(brand) {
this.brand = brand;
this.speed = 0;
}
const car = new Vehicle("Toyota");
// car.__proto__ points to Vehicle.prototype
console.log(car.__proto__ === Vehicle.prototype); // true
// Vehicle.prototype.__proto__ points to Object.prototype
console.log(Vehicle.prototype.__proto__ === Object.prototype); // true
// Object.prototype.__proto__ is null, the end of the prototype chain
console.log(Object.prototype.__proto__ === null); // trueMemory Tips:
prototypebelongs to functions and is the function's "design blueprint"__proto__belongs to objects and is the object's "birth certificate"
Prototype Chain Lookup Mechanism: A Treasure Hunt Journey
When you access an object's property, the JavaScript engine executes a precise lookup process:
- First check the object itself: See if the object has the property
- Search up the prototype chain: If not found, look at the prototype object pointed to by
__proto__ - Continue tracing up: Repeat step 2 until the property is found or
nullis reached
function LivingBeing() {
this.isAlive = true;
this.bornTime = Date.now();
}
LivingBeing.prototype.breathe = function () {
console.log(`${this.constructor.name} is breathing...`);
};
LivingBeing.prototype.getAge = function () {
const age = Math.floor((Date.now() - this.bornTime) / 1000);
return `${age} seconds old`;
};
function Animal(type, species) {
LivingBeing.call(this);
this.type = type;
this.species = species;
}
// Establish prototype chain: Animal.prototype -> LivingBeing.prototype
Animal.prototype = Object.create(LivingBeing.prototype);
Animal.prototype.constructor = Animal;
Animal.prototype.speak = function () {
console.log(`${this.species} makes a sound`);
};
Animal.prototype.move = function (speed) {
console.log(`${this.species} moves at ${speed} km/h`);
};
function Dog(name, breed) {
Animal.call(this, "mammal", "dog");
this.name = name;
this.breed = breed;
}
// Establish prototype chain: Dog.prototype -> Animal.prototype -> LivingBeing.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {
console.log(`${this.name} barks: Woof! Woof!`);
};
Dog.prototype.wagTail = function () {
console.log(`${this.name} wags tail happily`);
};
const myDog = new Dog("Max", "Golden Retriever");
// Let's trace the property lookup process:
console.log(myDog.name); // "Max" - found on the myDog object
// Lookup path for the bark method:
// 1. myDog.bark - check on myDog object → not found
// 2. myDog.__proto__ (Dog.prototype).bark - found!
myDog.bark(); // "Max barks: Woof! Woof!"
// Lookup path for the speak method:
// 1. myDog.speak - check on myDog object → not found
// 2. Dog.prototype.speak - check on Dog.prototype → not found
// 3. Animal.prototype.speak - check on Animal.prototype → found!
myDog.speak(); // "dog makes a sound"
// Lookup path for the breathe method:
// 1. myDog.breathe - not found
// 2. Dog.prototype.breathe - not found
// 3. Animal.prototype.breathe - not found
// 4. LivingBeing.prototype.breathe - found!
myDog.breathe(); // "Dog is breathing..."
// Lookup path for the getAge method:
// 1. myDog.getAge - not found
// 2. Dog.prototype.getAge - not found
// 3. Animal.prototype.getAge - not found
// 4. LivingBeing.prototype.getAge - found!
console.log(myDog.getAge()); // "X seconds old"
// Lookup path for the toString method:
// Found only when reaching Object.prototype
console.log(myDog.toString()); // "[object Object]"This lookup mechanism is like searching for a book in a library: first check your own bookshelf, then check the classroom library corner, then the school library, and finally find it in the city library.
Practical Applications of Prototype Chains
1. Method Sharing: The Wisdom of Saving Memory
The most common use of prototype chains is to let multiple objects share methods, which can save a lot of memory:
function Smartphone(brand, model, storage) {
this.brand = brand;
this.model = model;
this.storage = storage;
this.apps = [];
}
// Put methods on the prototype, shared by all instances
Smartphone.prototype.powerOn = function () {
console.log(`${this.brand} ${this.model} is booting up...`);
};
Smartphone.prototype.installApp = function (appName, size) {
if (this.storage >= size) {
this.apps.push({ name: appName, size: size });
this.storage -= size;
console.log(`${appName} installed successfully`);
} else {
console.log(`Not enough storage. Need ${size}MB, have ${this.storage}MB`);
}
};
Smartphone.prototype.listApps = function () {
console.log(`Installed apps on ${this.brand} ${this.model}:`);
this.apps.forEach((app) => console.log(`- ${app.name} (${app.size}MB)`));
};
Smartphone.prototype.uninstallApp = function (appName) {
const appIndex = this.apps.findIndex((app) => app.name === appName);
if (appIndex !== -1) {
const app = this.apps[appIndex];
this.apps.splice(appIndex, 1);
this.storage += app.size;
console.log(`${appName} uninstalled successfully`);
} else {
console.log(`${appName} not found`);
}
};
// Create multiple instances
const iPhone = new Smartphone("Apple", "iPhone 15", 128);
const galaxy = new Smartphone("Samsung", "Galaxy S24", 256);
// All instances share the same methods
iPhone.powerOn(); // "Apple iPhone 15 is booting up..."
galaxy.powerOn(); // "Samsung Galaxy S24 is booting up..."
// Use the shared methods
iPhone.installApp("WhatsApp", 150);
iPhone.installApp("Instagram", 200);
galaxy.installApp("TikTok", 300);
iPhone.listApps();
galaxy.listApps();
// Verify that methods are indeed shared
console.log(iPhone.powerOn === galaxy.powerOn); // true
console.log(iPhone.installApp === galaxy.installApp); // true
// Memory efficiency: if you have 10,000 instances, you only need one method copy
// Instead of each instance having its own method copy2. Property Overriding: Balancing Individuality and Commonality
Child objects can override properties on the prototype while still accessing parent properties through the prototype chain:
function Employee(name, position) {
this.name = name;
this.position = position;
this.workingHours = 40;
}
Employee.prototype.department = "General";
Employee.prototype.salary = 50000;
Employee.prototype.getJobInfo = function () {
return `${this.position} in ${this.department} department`;
};
Employee.prototype.requestLeave = function (days) {
console.log(`${this.name} is requesting ${days} days leave`);
};
function Manager(name, position, teamSize) {
Employee.call(this, name, position);
this.teamSize = teamSize;
}
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
// Override properties on the prototype
Manager.prototype.department = "Management";
Manager.prototype.salary = 80000;
Manager.prototype.workingHours = 45;
// Add manager-specific methods
Manager.prototype.approveLeave = function (employee, days) {
console.log(
`Manager ${this.name} approved ${employee.name}'s ${days} days leave`
);
};
Manager.prototype.runTeamMeeting = function () {
console.log(
`${this.name} is conducting a team meeting for ${this.teamSize} team members`
);
};
const employee1 = new Employee("John Doe", "Developer");
const manager1 = new Manager("Jane Smith", "Senior Manager", 8);
console.log(employee1.getJobInfo()); // "Developer in General department"
console.log(manager1.getJobInfo()); // "Senior Manager in Management department"
console.log(employee1.salary); // 50000 (from prototype)
console.log(manager1.salary); // 80000 (overrode the prototype's value)
// Can still use methods on the prototype
employee1.requestLeave(5); // "John Doe is requesting 5 days leave"
manager1.requestLeave(3); // "Jane Smith is requesting 3 days leave"
// Manager-specific methods
manager1.approveLeave(employee1, 5); // "Manager Jane Smith approved John Doe's 5 days leave"
manager1.runTeamMeeting(); // "Jane Smith is conducting a team meeting for 8 team members"3. Checking Property Sources: Distinguishing Own vs Inherited
Use the hasOwnProperty method to distinguish whether properties are the object's own or inherited:
function Task(title, priority) {
this.title = title;
this.priority = priority;
this.completed = false;
}
Task.prototype.category = "General";
Task.prototype.description = "Default task description";
Task.prototype.toggleComplete = function () {
this.completed = !this.completed;
console.log(
`Task "${this.title}" marked as ${this.completed ? "completed" : "pending"}`
);
};
Task.prototype.getDetails = function () {
return `${this.title} (${this.priority} priority) - ${
this.completed ? "Done" : "Pending"
}`;
};
const task1 = new Task("Write report", "High");
// Check property sources
console.log(task1.hasOwnProperty("title")); // true (own property)
console.log(task1.hasOwnProperty("priority")); // true (own property)
console.log(task1.hasOwnProperty("completed")); // true (own property)
console.log(task1.hasOwnProperty("category")); // false (inherited property)
console.log(task1.hasOwnProperty("description")); // false (inherited property)
console.log(task1.hasOwnProperty("toggleComplete")); // false (inherited method)
// The 'in' operator searches the entire prototype chain
console.log("title" in task1); // true
console.log("category" in task1); // true
console.log("toggleComplete" in task1); // true
// Custom function to determine property types
function getPropertyInfo(obj, prop) {
const hasOwn = obj.hasOwnProperty(prop);
const inProto = prop in obj;
if (hasOwn) {
return `${prop}: own property`;
} else if (inProto) {
return `${prop}: inherited property`;
} else {
return `${prop}: does not exist`;
}
}
console.log(getPropertyInfo(task1, "title")); // "title: own property"
console.log(getPropertyInfo(task1, "category")); // "category: inherited property"
console.log(getPropertyInfo(task1, "nonexistent")); // "nonexistent: does not exist"Advanced Prototype Chain Operations
1. Dynamic Prototype Modification: Adding Capabilities in Real Time
function Calculator() {
this.result = 0;
}
Calculator.prototype.add = function (number) {
this.result += number;
return this;
};
Calculator.prototype.subtract = function (number) {
this.result -= number;
return this;
};
Calculator.prototype.getResult = function () {
return this.result;
};
const calc = new Calculator();
calc.add(10).subtract(3).add(5);
console.log(calc.getResult()); // 12
// Dynamically add new methods to the prototype
Calculator.prototype.multiply = function (number) {
this.result *= number;
return this;
};
Calculator.prototype.divide = function (number) {
if (number !== 0) {
this.result /= number;
} else {
console.log("Cannot divide by zero");
}
return this;
};
calc.multiply(2).divide(4);
console.log(calc.getResult()); // 4.5
// Existing calculator instances can also use new methods
const calc2 = new Calculator();
calc2.add(20).multiply(3);
console.log(calc2.getResult()); // 602. Prototype Chain Detection: Understanding Object Relationships
// Get an object's prototype chain
function getPrototypeChain(obj) {
const chain = [];
let current = obj;
while (current !== null) {
chain.push(current.constructor.name || "Object");
current = Object.getPrototypeOf(current);
}
return chain.join(" -> ");
}
function Shape(name) {
this.name = name;
}
function Circle(radius) {
Shape.call(this, "Circle");
this.radius = radius;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
function ColoredCircle(radius, color) {
Circle.call(this, radius);
this.color = color;
}
ColoredCircle.prototype = Object.create(Circle.prototype);
ColoredCircle.prototype.constructor = ColoredCircle;
const coloredCircle = new ColoredCircle(5, "red");
console.log(getPrototypeChain(coloredCircle)); // "ColoredCircle -> Circle -> Shape -> Object"
// Check if object is an instance of a constructor
console.log(coloredCircle instanceof ColoredCircle); // true
console.log(coloredCircle instanceof Circle); // true
console.log(coloredCircle instanceof Shape); // true
console.log(coloredCircle instanceof Array); // false
// More precise prototype checking
function isPrototypeOf(Constructor, obj) {
return obj.constructor === Constructor;
}
console.log(isPrototypeOf(ColoredCircle, coloredCircle)); // true
console.log(isPrototypeOf(Circle, coloredCircle)); // falsePerformance Considerations for Prototype Chains
1. Avoid Deep Prototype Chains
Deep prototype chains can affect property lookup performance, so you should maintain reasonable depth:
// ❌ Not recommended: overly deep prototype chain
function Level1() {}
function Level2() {}
function Level3() {}
function Level4() {}
function Level5() {}
function Level6() {}
Level2.prototype = Object.create(Level1.prototype);
Level3.prototype = Object.create(Level2.prototype);
Level4.prototype = Object.create(Level3.prototype);
Level5.prototype = Object.create(Level4.prototype);
Level6.prototype = Object.create(Level5.prototype);
const deepObj = new Level6();
// Looking up deep properties requires traversing the entire prototype chain, which performs poorly
// ✅ Recommended: reasonable prototype chain depth (typically no more than 3-4 levels)
function BaseClass() {}
function MiddleClass() {}
function TopClass() {}
MiddleClass.prototype = Object.create(BaseClass.prototype);
TopClass.prototype = Object.create(MiddleClass.prototype);
const reasonableObj = new TopClass();
// Property lookup only needs to traverse a shorter prototype chain2. Prototype Chain Pollution Risks
// ❌ Dangerous: modifying Object.prototype
// This affects all objects and may lead to unexpected behavior
Object.prototype.someDangerousMethod = function () {
console.log("This method affects all objects!");
};
const obj = { a: 1 };
obj.someDangerousMethod(); // Now all objects have this method
// ✅ Safe: create your own utility objects
const StringUtils = {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
},
reverse(str) {
return str.split("").reverse().join("");
},
truncate(str, length) {
return str.length > length ? str.slice(0, length) + "..." : str;
},
};
// Use utility objects instead of polluting global prototypes
const text = "hello world";
console.log(StringUtils.capitalize(text)); // "Hello world"3. Timing of Prototype Modification
function Person(name) {
this.name = name;
}
const john = new Person("John");
// Modify the prototype after creating instances
Person.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
// The john object can access the newly added method
// Because JavaScript dynamically looks up the prototype chain
john.sayHello(); // "Hello, I'm John"
// But it's better practice to complete all prototype setup before creating instancesModern JavaScript Alternatives
While prototype chains are a core mechanism of JavaScript, we usually use higher-level syntax in modern JavaScript development.
1. Class Syntax: Modern Wrapper for Prototypes
// Modern Class syntax (still prototype chains underneath)
class Animal {
constructor(type) {
this.type = type;
}
speak() {
console.log(`${this.type} makes a sound`);
}
move(speed) {
console.log(`${this.type} moves at ${speed} km/h`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super("dog"); // Call parent constructor
this.name = name;
this.breed = breed;
}
bark() {
console.log(`${this.name} (${this.breed}) barks: Woof!`);
}
// Override parent method
speak() {
console.log(`${this.name} barks loudly`);
}
// Call parent method
getDetails() {
return `${this.name} is a ${this.breed} dog`;
}
}
const myDog = new Dog("Max", "Golden Retriever");
myDog.speak(); // Overridden method
myDog.move(15); // Inherited method
myDog.bark(); // Own method
console.log(myDog.getDetails()); // "Max is a Golden Retriever dog"2. Object.create(): Directly Create Prototype Objects
// Create objects with specified prototypes
const animalProto = {
type: "unknown",
speak() {
console.log(`${this.type} makes a sound`);
},
move(speed) {
console.log(`${this.type} moves at ${speed} km/h`);
},
};
const cat = Object.create(animalProto);
cat.type = "cat";
cat.meow = function () {
console.log("Cat meows: Meow!");
};
cat.speak(); // Inherited method
cat.meow(); // Own method
// Create more complex prototype chains
const mammalProto = Object.create(animalProto, {
type: { value: "mammal" },
warmBlooded: { value: true },
});
mammalProto.giveBirth = function () {
console.log(`${this.type} gives birth to live young`);
};
const dog = Object.create(mammalProto);
dog.type = "dog";
dog.bark = function () {
console.log("Dog barks: Woof!");
};
dog.speak(); // "dog makes a sound" (from animalProto)
dog.giveBirth(); // "dog gives birth to live young" (from mammalProto)
dog.bark(); // "Dog barks: Woof!" (own method)3. Object.setPrototypeOf(): Dynamically Set Prototypes
const baseObject = {
commonMethod() {
console.log("This is a common method");
},
};
const extendedObject = {
specificMethod() {
console.log("This is a specific method");
},
};
// Dynamically set prototype
Object.setPrototypeOf(extendedObject, baseObject);
extendedObject.commonMethod(); // Can access base object's methods
extendedObject.specificMethod(); // Can access its own methods
// Verify prototype relationship
console.log(extendedObject.__proto__ === baseObject); // trueSummary: Master the Power of Prototypes
Prototypes and prototype chains are the foundational mechanisms for implementing property and method sharing between JavaScript objects. Understanding how they work is crucial for mastering JavaScript:
Core Concepts Review
- Every object has a prototype: Accessed via
__proto__property - Every function has a prototype property: Determines the prototype of objects created by that function
- Property lookup follows the prototype chain: Until found or reaches the chain end (null)
- Methods should be placed on prototypes: To achieve memory sharing and code reuse
- Use prototype chains reasonably: Can build efficient inheritance systems
Practical Application Advice
- Method sharing: Place common methods on prototypes for all instances to share
- Property overriding: Child objects can override prototype properties while retaining access to parent properties
- Performance optimization: Avoid deep prototype chains and maintain reasonable inheritance hierarchy
- Safe programming: Avoid polluting global prototypes and create dedicated utility objects
Although modern JavaScript provides friendlier syntax like Class, the underlying mechanism is still based on prototype chains. Understanding prototypes not only helps you use JavaScript better but also find solutions when facing complex problems, enabling you to write more efficient and elegant code.
Prototype chains are the essence of JavaScript; mastering them means truly understanding the nature of this language. In the world of JavaScript, there are no isolated objects—every object lives in the big family of the prototype chain.