Prototypes and Inheritance in JavaScript
JavaScript is a prototype-based language, which means inheritance and object creation work differently than in classical object-oriented languages like Java or C++. Understanding prototypes is crucial to mastering JavaScript!
🧬 What is a Prototype?
Every JavaScript object has a special hidden property called [[Prototype]] (often accessed via __proto__ or Object.getPrototypeOf()). This property is a reference to another object called its prototype.
const person = { name: "Alice", age: 30};
console.log(person.__proto__); // Object.prototypeconsole.log(Object.getPrototypeOf(person)); // Object.prototype (recommended way)[!IMPORTANT] When you try to access a property on an object, JavaScript first looks at the object itself. If it doesn’t find it, it looks at the object’s prototype, then the prototype’s prototype, and so on. This is called the prototype chain.
🔗 The Prototype Chain
The prototype chain is how JavaScript implements inheritance. It’s a series of links between objects.
const animal = { eats: true, walk() { console.log("Animal walks"); }};
const rabbit = { jumps: true};
// Set animal as the prototype of rabbitrabbit.__proto__ = animal;
console.log(rabbit.eats); // true (found in prototype)console.log(rabbit.jumps); // true (found in rabbit itself)rabbit.walk(); // Animal walks (method from prototype)How Property Lookup Works
const obj = { a: 1};
// Prototype chain: obj -> Object.prototype -> null
console.log(obj.a); // 1 (found in obj)console.log(obj.toString()); // [object Object] (found in Object.prototype)console.log(obj.nonExistent); // undefined (not found anywhere)console.log(Object.getPrototypeOf(obj)); // Object.prototypeconsole.log(Object.getPrototypeOf(Object.prototype)); // null (end of chain)🏗️ Constructor Functions and Prototypes
Before ES6 classes, constructor functions were the primary way to create objects with shared behavior.
function Person(name, age) { // Instance properties (unique to each object) this.name = name; this.age = age;}
// Shared methods (on the prototype)Person.prototype.greet = function() { console.log(`Hello, I'm ${this.name}, ${this.age} years old`);};
Person.prototype.birthday = function() { this.age++; console.log(`Happy birthday! Now ${this.age} years old`);};
const alice = new Person("Alice", 30);const bob = new Person("Bob", 25);
alice.greet(); // Hello, I'm Alice, 30 years oldbob.greet(); // Hello, I'm Bob, 25 years oldalice.birthday(); // Happy birthday! Now 31 years old
// Both share the same methodconsole.log(alice.greet === bob.greet); // trueWhy Use Prototype for Methods?
// ❌ Bad: Each instance gets its own copyfunction PersonBad(name) { this.name = name; this.greet = function() { console.log(`Hi, I'm ${this.name}`); };}
const p1 = new PersonBad("Alice");const p2 = new PersonBad("Bob");console.log(p1.greet === p2.greet); // false (memory waste!)
// ✅ Good: All instances share one methodfunction PersonGood(name) { this.name = name;}
PersonGood.prototype.greet = function() { console.log(`Hi, I'm ${this.name}`);};
const p3 = new PersonGood("Alice");const p4 = new PersonGood("Bob");console.log(p3.greet === p4.greet); // true (efficient!)🎯 Understanding prototype vs __proto__
This is one of the most confusing aspects of JavaScript!
function Dog(name) { this.name = name;}
Dog.prototype.bark = function() { console.log(`${this.name} says Woof!`);};
const myDog = new Dog("Buddy");Key Differences
| Property | What is it? | On which objects? |
|---|---|---|
prototype | An object that will become the __proto__ of instances | Functions only |
__proto__ | Reference to the actual prototype object | All objects |
// Function's prototype propertyconsole.log(Dog.prototype); // { bark: function, constructor: Dog }
// Instance's __proto__ propertyconsole.log(myDog.__proto__); // Same as Dog.prototypeconsole.log(myDog.__proto__ === Dog.prototype); // true
// The constructor propertyconsole.log(Dog.prototype.constructor === Dog); // trueconsole.log(myDog.constructor === Dog); // true (inherited from prototype)Visual Representation
Dog (constructor function) └─ prototype: { bark: function, constructor: Dog } ↑ | __proto__ |myDog (instance) └─ name: "Buddy" └─ __proto__ ─────┘🎨 Object.create()
Object.create() is a modern way to set up prototype chains without constructor functions.
const personPrototype = { greet() { console.log(`Hello, I'm ${this.name}`); }, introduce() { console.log(`My name is ${this.name} and I'm ${this.age}`); }};
// Create object with personPrototype as prototypeconst alice = Object.create(personPrototype);alice.name = "Alice";alice.age = 30;
alice.greet(); // Hello, I'm Alice
console.log(Object.getPrototypeOf(alice) === personPrototype); // trueObject.create() vs Constructor Functions
// Using Object.create()const animal = { eat() { console.log("Eating..."); }};
const dog = Object.create(animal);dog.bark = function() { console.log("Woof!");};
// Using Constructor Functionfunction Animal() {}Animal.prototype.eat = function() { console.log("Eating...");};
function Dog() {}Dog.prototype = Object.create(Animal.prototype);Dog.prototype.constructor = Dog;Dog.prototype.bark = function() { console.log("Woof!");};🔄 Inheritance Patterns
Pattern 1: Prototypal Inheritance
function Animal(name) { this.name = name;}
Animal.prototype.eat = function() { console.log(`${this.name} is eating`);};
function Dog(name, breed) { Animal.call(this, name); // Call parent constructor this.breed = breed;}
// Set up prototype chainDog.prototype = Object.create(Animal.prototype);Dog.prototype.constructor = Dog;
// Add Dog-specific methodsDog.prototype.bark = function() { console.log(`${this.name} says Woof!`);};
const myDog = new Dog("Buddy", "Golden Retriever");myDog.eat(); // Buddy is eating (inherited from Animal)myDog.bark(); // Buddy says Woof!
console.log(myDog instanceof Dog); // trueconsole.log(myDog instanceof Animal); // truePattern 2: Object.create() Inheritance
const animal = { init(name) { this.name = name; return this; }, eat() { console.log(`${this.name} is eating`); }};
const dog = Object.create(animal);dog.bark = function() { console.log(`${this.name} says Woof!`);};
const myDog = Object.create(dog).init("Buddy");myDog.eat(); // Buddy is eatingmyDog.bark(); // Buddy says Woof!Pattern 3: ES6 Classes (Syntactic Sugar)
class Animal { constructor(name) { this.name = name; }
eat() { console.log(`${this.name} is eating`); }}
class Dog extends Animal { constructor(name, breed) { super(name); // Call parent constructor this.breed = breed; }
bark() { console.log(`${this.name} says Woof!`); }}
const myDog = new Dog("Buddy", "Golden Retriever");myDog.eat(); // Buddy is eatingmyDog.bark(); // Buddy says Woof![!NOTE] ES6 classes are just syntactic sugar over prototypes! Under the hood, they use the same prototype-based inheritance.
🔍 Checking Prototypes
instanceof Operator
function Person() {}const person = new Person();
console.log(person instanceof Person); // trueconsole.log(person instanceof Object); // true (everything inherits from Object)
const arr = [];console.log(arr instanceof Array); // trueconsole.log(arr instanceof Object); // trueisPrototypeOf() Method
function Person() {}const person = new Person();
console.log(Person.prototype.isPrototypeOf(person)); // trueconsole.log(Object.prototype.isPrototypeOf(person)); // true
const obj1 = { a: 1 };const obj2 = Object.create(obj1);
console.log(obj1.isPrototypeOf(obj2)); // truehasOwnProperty() Method
function Person(name) { this.name = name;}
Person.prototype.greet = function() { console.log(`Hello, ${this.name}`);};
const alice = new Person("Alice");
console.log(alice.hasOwnProperty('name')); // true (own property)console.log(alice.hasOwnProperty('greet')); // false (inherited)console.log('greet' in alice); // true (exists in chain)🛠️ Modifying Prototypes
Adding Methods to Built-in Prototypes
// ⚠️ Generally not recommended, but possibleArray.prototype.last = function() { return this[this.length - 1];};
const numbers = [1, 2, 3, 4, 5];console.log(numbers.last()); // 5
const fruits = ['apple', 'banana', 'orange'];console.log(fruits.last()); // orange[!CAUTION] Modifying built-in prototypes (like Array.prototype, Object.prototype) is generally considered bad practice as it can cause conflicts with other code or future JavaScript features!
Safer Alternative: Extend Your Own Objects
function MyArray() { this.items = [];}
MyArray.prototype.add = function(item) { this.items.push(item);};
MyArray.prototype.last = function() { return this.items[this.items.length - 1];};
const myArr = new MyArray();myArr.add(1);myArr.add(2);myArr.add(3);console.log(myArr.last()); // 3💡 Practical Examples
Example 1: Creating a Shape Hierarchy
function Shape(color) { this.color = color;}
Shape.prototype.describe = function() { console.log(`A ${this.color} shape`);};
function Circle(color, radius) { Shape.call(this, color); this.radius = radius;}
Circle.prototype = Object.create(Shape.prototype);Circle.prototype.constructor = Circle;
Circle.prototype.area = function() { return Math.PI * this.radius ** 2;};
Circle.prototype.describe = function() { console.log(`A ${this.color} circle with radius ${this.radius}`);};
function Rectangle(color, width, height) { Shape.call(this, color); this.width = width; this.height = height;}
Rectangle.prototype = Object.create(Shape.prototype);Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.area = function() { return this.width * this.height;};
Rectangle.prototype.describe = function() { console.log(`A ${this.color} rectangle ${this.width}x${this.height}`);};
const circle = new Circle("red", 5);const rectangle = new Rectangle("blue", 4, 6);
circle.describe(); // A red circle with radius 5console.log(circle.area()); // 78.53981633974483
rectangle.describe(); // A blue rectangle 4x6console.log(rectangle.area()); // 24Example 2: Plugin System
function App(name) { this.name = name; this.plugins = [];}
App.prototype.use = function(plugin) { this.plugins.push(plugin); plugin.init(this); return this;};
App.prototype.run = function() { console.log(`${this.name} is running with ${this.plugins.length} plugins`);};
// Plugin constructorfunction Logger() {}
Logger.prototype.init = function(app) { console.log(`Logger plugin initialized for ${app.name}`);};
function Analytics() {}
Analytics.prototype.init = function(app) { console.log(`Analytics plugin initialized for ${app.name}`);};
const myApp = new App("MyApp");myApp .use(new Logger()) .use(new Analytics()) .run();
// Logger plugin initialized for MyApp// Analytics plugin initialized for MyApp// MyApp is running with 2 plugins⚙️ Advanced Concepts
Prototype Pollution Attack
// ⚠️ Security concern: Prototype pollutionconst user = { name: "Alice" };
// Malicious code could modify Object.prototypeuser.__proto__.isAdmin = true;
const attacker = { name: "Hacker" };console.log(attacker.isAdmin); // true (inherited from polluted prototype!)
// ✅ Protection: Use Object.create(null)const safeUser = Object.create(null);safeUser.name = "Bob";console.log(safeUser.__proto__); // undefined (no prototype chain!)Shadowing Properties
const parent = { name: "Parent", greet() { console.log(`Hello from ${this.name}`); }};
const child = Object.create(parent);child.name = "Child"; // Shadows parent.name
console.log(child.name); // Child (own property)console.log(parent.name); // Parentdelete child.name;console.log(child.name); // Parent (now from prototype)Method Overriding
function Vehicle(type) { this.type = type;}
Vehicle.prototype.move = function() { console.log(`${this.type} is moving`);};
function Car(brand) { Vehicle.call(this, "Car"); this.brand = brand;}
Car.prototype = Object.create(Vehicle.prototype);Car.prototype.constructor = Car;
// Override parent methodCar.prototype.move = function() { console.log(`${this.brand} car is driving on the road`);};
const myCar = new Car("Toyota");myCar.move(); // Toyota car is driving on the road🎭 Prototype vs Class
| Aspect | Prototypes | Classes |
|---|---|---|
| Syntax | Function-based | Class-based (ES6+) |
| Readability | More complex | Cleaner, more familiar |
| Hoisting | Functions are hoisted | Classes are NOT hoisted |
| Strict Mode | Optional | Always in strict mode |
| Underlying Mechanism | Direct | Syntactic sugar over prototypes |
// Both are equivalent!
// Prototype stylefunction Person(name) { this.name = name;}Person.prototype.greet = function() { console.log(`Hi, I'm ${this.name}`);};
// Class styleclass Person { constructor(name) { this.name = name; } greet() { console.log(`Hi, I'm ${this.name}`); }}✅ Best Practices
- Prefer ES6 Classes for New Code: They’re cleaner and easier to understand.
// ✅ Modern and cleanclass User { constructor(name) { this.name = name; }
greet() { console.log(`Hello, ${this.name}`); }}- Use Object.create() for Simple Inheritance: When you don’t need constructor functions.
// ✅ Simple and effectiveconst parent = { greet() { console.log("Hello"); } };const child = Object.create(parent);- Don’t Modify Built-in Prototypes: Avoid extending native objects.
// ❌ BadArray.prototype.myMethod = function() { /* ... */ };
// ✅ Goodclass MyArray extends Array { myMethod() { /* ... */ }}- Use
Object.getPrototypeOf()instead of__proto__: It’s the standard way.
// ❌ Avoidconst proto = obj.__proto__;
// ✅ Preferconst proto = Object.getPrototypeOf(obj);- Set constructor property correctly: When manually creating prototype chains.
function Child() {}Child.prototype = Object.create(Parent.prototype);Child.prototype.constructor = Child; // ✅ Don't forget this!🎯 Real-World Example: Event Emitter
function EventEmitter() { this.events = {};}
EventEmitter.prototype.on = function(event, listener) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(listener); return this;};
EventEmitter.prototype.emit = function(event, ...args) { if (this.events[event]) { this.events[event].forEach(listener => listener(...args)); } return this;};
EventEmitter.prototype.off = function(event, listenerToRemove) { if (this.events[event]) { this.events[event] = this.events[event].filter( listener => listener !== listenerToRemove ); } return this;};
// Usageconst emitter = new EventEmitter();
const onLogin = (user) => console.log(`${user} logged in`);const onLogout = (user) => console.log(`${user} logged out`);
emitter .on('login', onLogin) .on('logout', onLogout) .on('login', (user) => console.log(`Welcome, ${user}!`));
emitter.emit('login', 'Alice');// Alice logged in// Welcome, Alice!
emitter.emit('logout', 'Alice');// Alice logged out📚 Summary
- Every JavaScript object has a prototype (
[[Prototype]]) - Prototypes enable inheritance through the prototype chain
- Constructor functions use
.prototypeto share methods Object.create()provides a cleaner way to set up prototypes- ES6 classes are syntactic sugar over prototypes
- Understanding prototypes is key to mastering JavaScript’s object model
🔗 Resources
- MDN: Object prototypes
- MDN: Inheritance and the prototype chain
- JavaScript.info: Prototypal inheritance
Happy Coding! Now you understand how JavaScript really works! 🚀