Prototypal Inheritance in JavaScript
Inheritance is a core concept in programming: the ability for one object to access properties and methods defined on another. Most languages you may have encountered use class-based inheritance, where blueprints (classes) define what objects look like. JavaScript takes a fundamentally different approach. In JavaScript, objects inherit directly from other objects through a mechanism called prototypal inheritance.
Every object in JavaScript has a hidden link to another object called its prototype. When you access a property that does not exist on the object itself, JavaScript automatically looks at its prototype, then the prototype's prototype, and so on up the chain. Understanding this mechanism is essential because it powers everything in JavaScript, from simple method calls like "hello".toUpperCase() to the entire class syntax introduced in ES6.
This guide walks you through prototypal inheritance step by step, from the internal mechanics to the practical patterns and pitfalls you will encounter in real code.
The [[Prototype]] Internal Slot
Every object in JavaScript has a hidden internal property called [[Prototype]]. This is not a regular property you can access with dot notation. It is an internal slot defined by the ECMAScript specification. It holds a reference to another object (or null if the object has no prototype).
Think of [[Prototype]] as a secret arrow pointing from one object to another:
myObject → [[Prototype]] → parentObject → [[Prototype]] → Object.prototype → [[Prototype]] → null
When JavaScript looks up a property on an object and does not find it, it follows this arrow to the prototype, checks there, follows that object's arrow, and so on. This continues until it either finds the property or reaches null (the end of the chain).
Here is the key idea: objects can "inherit" properties from other objects without copying them. The properties stay on the prototype, and the child object simply has a link to find them.
const animal = {
eats: true,
walk() {
console.log("Animal walks");
}
};
const rabbit = {
jumps: true
};
// Set animal as the prototype of rabbit
Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Animal walks" (inherited method)
The rabbit object does not have an eats property or a walk method. But because its [[Prototype]] points to animal, JavaScript finds them there.
__proto__: The Legacy Accessor (and Why to Avoid It)
Historically, the only way to access an object's [[Prototype]] in code was through the __proto__ property. This was not part of the official specification for a long time. It was a non-standard extension implemented by browsers, and it was eventually standardized in ES2015 only for backward compatibility.
const animal = {
eats: true
};
const rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
console.log(rabbit.eats); // true (inherited)
console.log(rabbit.jumps); // true (own property)
You can also set __proto__ directly inside an object literal:
const animal = {
eats: true
};
const rabbit = {
jumps: true,
__proto__: animal // Set prototype in the literal
};
console.log(rabbit.eats); // true
Why You Should Avoid __proto__
Despite being widely supported, __proto__ has several problems:
-
It is a legacy feature. The specification marks it as "normative optional," meaning environments are not strictly required to support it.
-
Performance. Changing an object's prototype after creation (
Object.setPrototypeOf()or__proto__assignment) is a very slow operation in modern engines. It invalidates internal optimizations that the engine has built around the object's structure. -
Security concerns. Using
__proto__on objects created from user input can lead to prototype pollution attacks, where an attacker injects properties intoObject.prototypethrough a__proto__key in JSON data. -
It does not work on all objects. Objects created with
Object.create(null)have no prototype and no__proto__accessor.
The Modern Alternative
Use the proper API methods instead:
const animal = { eats: true };
const rabbit = { jumps: true };
// Setting prototype (prefer at creation time)
const rabbit2 = Object.create(animal);
rabbit2.jumps = true;
// Reading prototype
console.log(Object.getPrototypeOf(rabbit2) === animal); // true
// Setting prototype on existing object (avoid if possible)
Object.setPrototypeOf(rabbit, animal);
console.log(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf() works but triggers the same performance problems as __proto__ assignment. The best practice is to set the prototype at object creation time using Object.create(), not after the object already exists.
| Approach | Use Case | Recommendation |
|---|---|---|
Object.create(proto) | Create object with a specific prototype | Preferred |
Object.getPrototypeOf(obj) | Read an object's prototype | Preferred |
Object.setPrototypeOf(obj, proto) | Change prototype after creation | Use sparingly |
obj.__proto__ | Read/write prototype | Avoid |
Reading Properties Through the Prototype Chain
When you access a property on an object, JavaScript performs a property lookup that walks up the prototype chain. This is the core mechanism of prototypal inheritance.
How the Lookup Works
- Check if the object itself has the property (an own property)
- If not found, check the object's
[[Prototype]] - If not found there, check that prototype's
[[Prototype]] - Continue until the property is found or
nullis reached - If
nullis reached, returnundefined
const base = {
type: "base",
greet() {
return "Hello from base";
}
};
const middle = Object.create(base);
middle.color = "blue";
const child = Object.create(middle);
child.name = "child object";
// Own property
console.log(child.name); // "child object" (found on child itself)
// Inherited from middle
console.log(child.color); // "blue" (not on child, found on middle)
// Inherited from base (two levels up)
console.log(child.type); // "base" (not on child, not on middle, found on base)
console.log(child.greet()); // "Hello from base"
// Not found anywhere in the chain
console.log(child.missing); // undefined
The Chain Has No Length Limit
You can chain as many objects as you want, though in practice, chains rarely go deeper than a few levels:
const a = { fromA: "A" };
const b = Object.create(a);
b.fromB = "B";
const c = Object.create(b);
c.fromC = "C";
const d = Object.create(c);
d.fromD = "D";
console.log(d.fromD); // "D" (own)
console.log(d.fromC); // "C" (1 level up)
console.log(d.fromB); // "B" (2 levels up)
console.log(d.fromA); // "A" (3 levels up)
Methods Are Inherited Too
Methods on a prototype are shared by all objects that inherit from it. They are not copied; every inheriting object accesses the same function:
const animal = {
eats: true,
walk() {
console.log("Walking...");
}
};
const rabbit = Object.create(animal);
const hamster = Object.create(animal);
rabbit.walk(); // "Walking..."
hamster.walk(); // "Walking..."
// Both use the same function
console.log(rabbit.walk === hamster.walk); // true
console.log(rabbit.walk === animal.walk); // true
This is memory efficient. The walk function exists only once (on animal), not copied to each inheriting object.
Prototype Chain Rules
There are two important constraints:
1. No circular references. JavaScript throws an error if you try to create a cycle:
const a = {};
const b = Object.create(a);
try {
Object.setPrototypeOf(a, b); // Creating a cycle: a → b → a
} catch (e) {
console.log(e.message); // Cyclic __proto__ value
}
2. The prototype must be an object or null. Any other value is ignored:
const obj = {};
// These have no effect (prototype must be object or null)
Object.setPrototypeOf(obj, 42); // Throws TypeError
Object.setPrototypeOf(obj, "string"); // Throws TypeError
Object.setPrototypeOf(obj, undefined); // Throws TypeError
// This works
Object.setPrototypeOf(obj, null); // obj now has no prototype
Writing Properties: Shadow Properties
Reading properties walks up the prototype chain. Writing properties does not. When you assign a value to a property, it always creates or modifies an own property on the target object, even if a property with the same name exists on the prototype.
This is called property shadowing: the own property "shadows" (hides) the inherited one.
const animal = {
eats: true,
sound: "generic sound"
};
const cat = Object.create(animal);
console.log(cat.sound); // "generic sound" (inherited)
console.log(cat.hasOwnProperty("sound")); // false (it's on the prototype9
// Writing creates an own property on cat
cat.sound = "meow";
console.log(cat.sound); // "meow" (own property (shadows the inherited one))
console.log(animal.sound); // "generic sound" (prototype is unchanged)
console.log(cat.hasOwnProperty("sound")); // true (now it's own)
The prototype's sound property still exists and is unchanged. The cat object now has its own sound that takes priority during lookup.
The Pitfall: Inherited Objects and Arrays Are Shared
This write-to-own behavior has a critical subtlety. When the inherited property is an object or array, and you mutate it (instead of replacing it), you are modifying the prototype's property directly because no new own property is created.
The problem:
const animal = {
stomach: []
};
const rabbit = Object.create(animal);
const hamster = Object.create(animal);
// This MUTATES the inherited array (does NOT create an own property)
rabbit.stomach.push("carrot");
console.log(rabbit.stomach); // ["carrot"]
console.log(hamster.stomach); // ["carrot"] (hamster sees it too!)
console.log(animal.stomach); // ["carrot"] (the prototype was modified)
// All three point to the same array
console.log(rabbit.stomach === hamster.stomach); // true
console.log(rabbit.stomach === animal.stomach); // true
This happens because rabbit.stomach is a read operation (it finds the array on the prototype), and .push() mutates that found array in place. No property assignment to rabbit ever occurred.
The fix: assign a new array to create an own property:
const animal = {
stomach: []
};
const rabbit = Object.create(animal);
const hamster = Object.create(animal);
// ASSIGNMENT creates an own property
rabbit.stomach = [...rabbit.stomach, "carrot"]; // Creates own array
hamster.stomach = [...hamster.stomach, "lettuce"]; // Creates own array
console.log(rabbit.stomach); // ["carrot"]
console.log(hamster.stomach); // ["lettuce"]
console.log(animal.stomach); // [] (prototype unchanged)
This is one of the most common sources of bugs with prototypal inheritance. If a prototype contains mutable values (arrays, objects), all inheriting objects share the same mutable reference. Either assign new values to create own properties, or initialize mutable properties in constructor functions.
Alternative Fix: Initialize in a Method
A common pattern is to use an initialization method that creates own properties:
const animal = {
init(name) {
this.name = name;
this.stomach = []; // Creates own array on each instance
return this;
},
eat(food) {
this.stomach.push(food);
}
};
const rabbit = Object.create(animal).init("White Rabbit");
const hamster = Object.create(animal).init("Hammy");
rabbit.eat("carrot");
hamster.eat("lettuce");
console.log(rabbit.stomach); // ["carrot"]
console.log(hamster.stomach); // ["lettuce"] (separate arrays)
this in Prototype Methods: Always the Calling Object
This is a crucial rule: this in a prototype method always refers to the object before the dot, not the object where the method is defined. The prototype provides the method, but this refers to the actual caller.
const animal = {
sleep() {
this.isSleeping = true;
},
wake() {
this.isSleeping = false;
},
describe() {
return `${this.name} is ${this.isSleeping ? "sleeping" : "awake"}`;
}
};
const cat = Object.create(animal);
cat.name = "Whiskers";
const dog = Object.create(animal);
dog.name = "Rex";
cat.sleep();
// this === cat, so cat.isSleeping = true
console.log(cat.describe()); // "Whiskers is sleeping"
console.log(dog.describe()); // "Rex is awake"
// The method is on animal, but 'this' was cat/dog
console.log(cat.hasOwnProperty("isSleeping")); // true (created on cat)
console.log(dog.hasOwnProperty("isSleeping")); // false (never called sleep())
console.log(animal.hasOwnProperty("isSleeping")); // false (not touched)
When cat.sleep() executes, this is cat. So this.isSleeping = true creates an own property on cat, not on animal. The prototype provides the method, but the calling object receives the state changes.
Why This Behavior Is Powerful
This design means prototype methods are truly shared. Hundreds of objects can inherit the same method, and each one operates on its own data:
const counter = {
increment() {
this.count = (this.count || 0) + 1;
},
getCount() {
return this.count || 0;
}
};
const counterA = Object.create(counter);
const counterB = Object.create(counter);
counterA.increment();
counterA.increment();
counterA.increment();
counterB.increment();
console.log(counterA.getCount()); // 3
console.log(counterB.getCount()); // 1
// The method is the same function
console.log(counterA.increment === counterB.increment); // true
Each object tracks its own count because this in increment() refers to the calling object.
A More Complex Example with this
const userMethods = {
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
setFullName(name) {
const parts = name.split(" ");
this.firstName = parts[0];
this.lastName = parts[1] || "";
},
greet() {
return `Hi, I'm ${this.getFullName()}`;
}
};
const alice = Object.create(userMethods);
alice.firstName = "Alice";
alice.lastName = "Smith";
const bob = Object.create(userMethods);
bob.firstName = "Bob";
bob.lastName = "Jones";
console.log(alice.greet()); // "Hi, I'm Alice Smith"
console.log(bob.greet()); // "Hi, I'm Bob Jones"
// Notice: this.getFullName() also uses this correctly
// because it's called as alice.getFullName() internally
In greet(), this is alice (or bob). When greet() calls this.getFullName(), the call is effectively alice.getFullName(), so this in getFullName() is also alice. The this binding always follows the object-before-the-dot rule.
The for...in Loop and hasOwnProperty
The for...in loop iterates over all enumerable properties, including inherited ones. This is a critical distinction from Object.keys(), which only returns own properties.
const animal = {
eats: true
};
const rabbit = Object.create(animal);
rabbit.jumps = true;
// for...in includes inherited properties
for (let key in rabbit) {
console.log(key);
}
// Output:
// "jumps"
// "eats"
// Object.keys returns only own properties
console.log(Object.keys(rabbit)); // ["jumps"]
Filtering Own Properties with hasOwnProperty
To distinguish own properties from inherited ones inside a for...in loop, use hasOwnProperty():
const animal = {
eats: true,
walk() {
console.log("Walking");
}
};
const rabbit = Object.create(animal);
rabbit.jumps = true;
rabbit.name = "White Rabbit";
for (let key in rabbit) {
if (rabbit.hasOwnProperty(key)) {
console.log(`Own: ${key}`);
} else {
console.log(`Inherited: ${key}`);
}
}
// Output:
// "Own: jumps"
// "Own: name"
// "Inherited: eats"
// "Inherited: walk"
Why Does hasOwnProperty Not Show Up in for...in?
You might wonder: hasOwnProperty is itself inherited from Object.prototype. Why does it not appear in the for...in loop?
Because it is non-enumerable:
const descriptor = Object.getOwnPropertyDescriptor(
Object.prototype,
"hasOwnProperty"
);
console.log(descriptor.enumerable); // false
All built-in methods on Object.prototype (toString, valueOf, hasOwnProperty, etc.) are non-enumerable. Only enumerable properties appear in for...in.
Modern Alternatives to for...in
In modern JavaScript, you rarely need for...in. Prefer these methods:
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
rabbit.name = "Bunny";
// Own enumerable keys (strings only)
console.log(Object.keys(rabbit)); // ["jumps", "name"]
// Own enumerable values
console.log(Object.values(rabbit)); // [true, "Bunny"]
// Own enumerable [key, value] pairs
console.log(Object.entries(rabbit)); // [["jumps", true], ["name", "Bunny"]]
// ALL own keys (including non-enumerable and symbols)
console.log(Object.getOwnPropertyNames(rabbit)); // ["jumps", "name"]
// ALL own keys including symbols
console.log(Reflect.ownKeys(rabbit)); // ["jumps", "name"]
None of these include inherited properties.
Use Object.keys(), Object.values(), or Object.entries() when you want to iterate over an object's own properties. Use for...in only when you specifically need inherited properties, and always pair it with hasOwnProperty() unless you intentionally want the full chain.
The Safer hasOwnProperty Call
In edge cases where an object might have its own hasOwnProperty property (or the object was created with Object.create(null)), the standard call can break:
const obj = Object.create(null);
obj.key = "value";
// This throws: obj.hasOwnProperty is not a function
try {
obj.hasOwnProperty("key");
} catch (e) {
console.log(e.message); // obj.hasOwnProperty is not a function
}
// Safe alternative
console.log(Object.prototype.hasOwnProperty.call(obj, "key")); // true
// Modern alternative (ES2022+)
console.log(Object.hasOwn(obj, "key")); // true
Object.hasOwn() is the modern replacement for hasOwnProperty(). It works on all objects, including those without a prototype.
Prototype Chain Visualization
Understanding the prototype chain becomes much easier when you can visualize it. Let us build a multi-level chain and trace every connection.
Building the Chain
const vehicle = {
hasEngine: true,
start() {
return `${this.type || "Vehicle"} starting...`;
}
};
const car = Object.create(vehicle);
car.wheels = 4;
car.type = "Car";
const electricCar = Object.create(car);
electricCar.batteryCapacity = "75 kWh";
electricCar.type = "Electric Car";
The Complete Chain Diagram
electricCar car vehicle Object.prototype null
┌──────────────────┐ ┌──────────────┐ ┌────────────────────┐ ┌─────────────────────┐
│ batteryCapacity: │ │ wheels: 4 │ │ hasEngine: true │ │ toString() │
│ "75 kWh" │ │ type: "Car" │ │ start() {...} │ │ valueOf() │
│ type: │ │ │ │ │ │ hasOwnProperty() │
│ "Electric Car" │ │ │ │ │ │ ... │
│ │ │ │ │ │ │ │
│ [[Prototype]] ──────► │ [[Prototype]] ──► │ [[Prototype]] ────────► │ [[Prototype]] ──────────► null
└──────────────────┘ └──────────────┘ └────────────────────┘ └─────────────────────┘
Tracing Property Lookups
// Direct own property
console.log(electricCar.batteryCapacity); // "75 kWh"
// Lookup: electricCar.batteryCapacity → found on electricCar
// Shadows car.type
console.log(electricCar.type); // "Electric Car"
// Lookup: electricCar.type → found on electricCar (shadows car.type)
// Inherited from car
console.log(electricCar.wheels); // 4
// Lookup: electricCar.wheels → not found → car.wheels → found
// Inherited from vehicle
console.log(electricCar.hasEngine); // true
// Lookup: electricCar → not found → car → not found → vehicle.hasEngine → found
// Inherited from vehicle, but 'this' is electricCar
console.log(electricCar.start()); // "Electric Car starting..."
// Lookup: electricCar → car → vehicle.start → found
// Execution: this === electricCar, so this.type === "Electric Car"
// Inherited from Object.prototype
console.log(electricCar.toString()); // "[object Object]"
// Lookup: electricCar → car → vehicle → Object.prototype.toString → found
// Not found anywhere
console.log(electricCar.fly); // undefined
// Lookup: electricCar → car → vehicle → Object.prototype → null → undefined
Verifying the Chain Programmatically
// Check prototype relationships
console.log(Object.getPrototypeOf(electricCar) === car); // true
console.log(Object.getPrototypeOf(car) === vehicle); // true
console.log(Object.getPrototypeOf(vehicle) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
// Walk the entire chain
function getPrototypeChain(obj) {
const chain = [];
let current = obj;
while (current !== null) {
chain.push(current);
current = Object.getPrototypeOf(current);
}
chain.push(null);
return chain;
}
const chain = getPrototypeChain(electricCar);
console.log(chain.length); // 5: electricCar, car, vehicle, Object.prototype, null
Listing All Available Properties
You can inspect which properties are own and which are inherited:
function describeProperties(obj) {
const own = Object.getOwnPropertyNames(obj);
const inherited = [];
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
for (const key of Object.getOwnPropertyNames(proto)) {
if (!own.includes(key) && !inherited.includes(key)) {
inherited.push(key);
}
}
proto = Object.getPrototypeOf(proto);
}
return { own, inherited };
}
console.log(describeProperties(electricCar));
// {
// own: ["batteryCapacity", "type"],
// inherited: ["wheels", "hasEngine", "start", "constructor", "toString", "valueOf", ...]
// }
The End of Every Chain: Object.prototype
Unless explicitly set otherwise, every object's prototype chain eventually reaches Object.prototype, which provides the fundamental methods all objects share:
const simpleObj = { x: 1 };
// These all come from Object.prototype
console.log(typeof simpleObj.toString); // "function"
console.log(typeof simpleObj.valueOf); // "function"
console.log(typeof simpleObj.hasOwnProperty); // "function"
console.log(typeof simpleObj.isPrototypeOf); // "function"
console.log(typeof simpleObj.propertyIsEnumerable); // "function"
The only object with null as its prototype is Object.prototype itself:
console.log(Object.getPrototypeOf(Object.prototype)); // null
You can also create objects with no prototype at all:
const bare = Object.create(null);
bare.key = "value";
console.log(Object.getPrototypeOf(bare)); // null
console.log(bare.toString); // undefined (no Object.prototype methods)
These "bare" or "dictionary" objects are useful when you need a pure key-value store without any inherited properties potentially interfering with your keys.
Summary
Prototypal inheritance is the foundation of JavaScript's object system. Every concept from here forward, including constructor functions, the class syntax, and built-in types, builds on these mechanics.
| Concept | Key Point |
|---|---|
[[Prototype]] | Hidden internal link from one object to another |
__proto__ | Legacy accessor for [[Prototype]], avoid in modern code |
Object.create(proto) | Create an object with a specified prototype (preferred) |
Object.getPrototypeOf(obj) | Read an object's prototype (preferred) |
| Property lookup | Walks up the prototype chain until found or null |
| Property writing | Always creates/modifies an own property, never touches the prototype |
| Shadow properties | Own properties hide inherited properties with the same name |
this in methods | Always refers to the calling object (before the dot), not the prototype |
for...in | Iterates over all enumerable properties, including inherited ones |
Object.keys() | Returns only own enumerable string keys |
Object.hasOwn(obj, key) | Modern check for own properties (replaces hasOwnProperty) |
| Shared mutable state | Inherited arrays/objects are shared references; mutations affect all inheritors |
Key rules to remember:
- Reading walks the chain, writing stays on the object
- Mutating an inherited object or array modifies the shared prototype value, so initialize mutable data as own properties
thisalways refers to the object that called the method, making prototype methods naturally reusable- Use
Object.create()to set prototypes at creation time, not__proto__orObject.setPrototypeOf()after the fact - Prefer
Object.keys()andObject.hasOwn()overfor...inwithhasOwnProperty()in modern code