Skip to main content

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:

  1. It is a legacy feature. The specification marks it as "normative optional," meaning environments are not strictly required to support it.

  2. 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.

  3. Security concerns. Using __proto__ on objects created from user input can lead to prototype pollution attacks, where an attacker injects properties into Object.prototype through a __proto__ key in JSON data.

  4. 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
warning

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.

ApproachUse CaseRecommendation
Object.create(proto)Create object with a specific prototypePreferred
Object.getPrototypeOf(obj)Read an object's prototypePreferred
Object.setPrototypeOf(obj, proto)Change prototype after creationUse sparingly
obj.__proto__Read/write prototypeAvoid

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

  1. Check if the object itself has the property (an own property)
  2. If not found, check the object's [[Prototype]]
  3. If not found there, check that prototype's [[Prototype]]
  4. Continue until the property is found or null is reached
  5. If null is reached, return undefined
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)
caution

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.

tip

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.

ConceptKey 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 lookupWalks up the prototype chain until found or null
Property writingAlways creates/modifies an own property, never touches the prototype
Shadow propertiesOwn properties hide inherited properties with the same name
this in methodsAlways refers to the calling object (before the dot), not the prototype
for...inIterates 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 stateInherited 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
  • this always refers to the object that called the method, making prototype methods naturally reusable
  • Use Object.create() to set prototypes at creation time, not __proto__ or Object.setPrototypeOf() after the fact
  • Prefer Object.keys() and Object.hasOwn() over for...in with hasOwnProperty() in modern code