Skip to content

The Prototype Chain and Inheritance

intermediate10 min read

When you access a property on an object and it doesn't exist, JavaScript doesn't return undefined immediately. It follows a hidden link to another object and looks there. Then follows that object's hidden link. And so on, until it reaches an object whose link is null. This chain of hidden links is the prototype chain, and it's the mechanism behind every method you've ever called on an array, string, or object.

Understanding this chain is the difference between knowing JavaScript and knowing JavaScript.

Mental Model

Imagine a family tree. When you ask someone "do you have this skill?", they first check themselves. If not, they ask their parent. The parent checks, then asks their parent. This continues until someone either has the skill or you reach the top ancestor (who has no parent). That's prototype lookup. Every object's [[Prototype]] is its "parent" in this lookup chain.

__proto__ vs .prototype — The Confusion

This is where most developers get lost. There are two completely different things with similar names:

__proto__ (or Object.getPrototypeOf(obj)): The internal [[Prototype]] link on every object. This is the hidden link JavaScript follows when looking up properties.

.prototype: A regular property that exists only on functions. When you call new Foo(), the new object's __proto__ is set to Foo.prototype.

function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function() { return "Woof!"; };

const rex = new Dog("Rex");

// rex.__proto__ === Dog.prototype  → true
// rex.prototype === undefined      → true (rex is not a function)
// Dog.__proto__ === Function.prototype → true (Dog IS a function)
Execution Trace
new Dog()
Create empty object {}
The 'new' keyword creates a fresh object
Link
{}.__proto__ = Dog.prototype
Set the prototype link to Dog.prototype
Bind
Dog.call({}, 'Rex')
Run constructor with 'this' bound to new object
Assign
{ name: 'Rex' }
this.name = name assigns to the new object
Return
rex = { name: 'Rex' }
Return the new object (unless constructor returns an object)

Property Lookup — The Walk

When you access rex.bark(), the engine performs the following lookup:

rex.bark();
// 1. Does rex own a property called "bark"? No.
// 2. Does rex.__proto__ (Dog.prototype) have "bark"? Yes. Call it.

rex.toString();
// 1. Does rex have "toString"? No.
// 2. Does Dog.prototype have "toString"? No.
// 3. Does Object.prototype have "toString"? Yes. Call it.

The chain for rex looks like:

rex → Dog.prototype → Object.prototype → null

Every lookup walks this chain. Property writes do NOT walk the chain — they always create/modify a property directly on the object (with one exception: setters).

Common Trap

Property assignment does not walk the prototype chain, but there's a subtle exception. If the prototype has a setter for that property, the setter runs instead of creating an own property. This can cause silent data loss:

const parent = {
  set value(v) { console.log("setter called, value discarded"); }
};
const child = Object.create(parent);
child.value = 42;
// Logs: "setter called, value discarded"
// child.value is undefined — no own property was created!

Object.create — Prototype Without Constructors

Object.create(proto) creates a new object with proto as its [[Prototype]]:

const animal = {
  speak() { return `${this.name} makes a sound`; }
};

const dog = Object.create(animal);
dog.name = "Rex";
dog.speak(); // "Rex makes a sound"

// dog.__proto__ === animal → true
// dog has no "speak" own property — it's inherited

Object.create(null) creates an object with no prototype at all — no toString, no hasOwnProperty, nothing:

const dict = Object.create(null);
dict.toString; // undefined — no prototype chain
dict["__proto__"]; // undefined — not special here

// Useful for dictionaries where keys might be
// "constructor", "toString", "__proto__", etc.
Why Object.create(null) matters for security

When you use a plain object {} as a dictionary, keys like "__proto__", "constructor", and "toString" can collide with inherited properties. In a prototype pollution attack, user input gets assigned to __proto__, modifying the prototype of all objects. Object.create(null) eliminates this attack surface entirely. This is why many library internals (and Map) use it for key-value storage.

The Complete Prototype Chains

// Array instance chain:
[1, 2, 3]
Array.prototype (has push, map, filter, etc.)
Object.prototype (has toString, hasOwnProperty, etc.)
null

// Function chain:
function foo() {}
// foo → Function.prototype → Object.prototype → null

// The twist:
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype) === null; // true

// The circular-looking part:
Function.__proto__ === Function.prototype; // true
// Function is an instance of itself. This is a bootstrap
// quirk — both Function and Object are created by the engine
// before normal object creation rules apply.

Why Modifying Built-in Prototypes Is Dangerous

// "Helpful" utility — DON'T DO THIS
Array.prototype.last = function() {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 — works!

// But now every for...in over any array includes "last":
const arr = [1, 2, 3];
for (const key in arr) {
  console.log(key); // "0", "1", "2", "last"
}

// And if any library also defines Array.prototype.last
// with different behavior, one silently overwrites the other.
// This is why MooTools broke the web and forced TC39
// to rename Array.prototype.flatten to .flat
The MooTools incident

In 2018, TC39 wanted to add Array.prototype.flatten() to the language. But MooTools (a popular library from 2007) had already monkey-patched Array.prototype.flatten with incompatible behavior. Shipping the new native method would break millions of sites still using MooTools. TC39 was forced to rename it to Array.prototype.flat(). This is called "don't break the web" — and it's why you never modify built-in prototypes.

Production Scenario: The hasOwnProperty Problem

// API returns user data as a plain object
const userData = JSON.parse(apiResponse);

// Bug: what if the API sends { "hasOwnProperty": "hacked" }?
userData.hasOwnProperty("name"); // TypeError: not a function

// Safe alternative:
Object.prototype.hasOwnProperty.call(userData, "name");

// Even better (ES2022):
Object.hasOwn(userData, "name"); // true/false, never throws
What developers doWhat they should do
Confusing __proto__ with .prototype
Every object has __proto__. Only functions have .prototype.
__proto__ is the lookup link on instances. .prototype is a property on constructor functions.
Modifying Array.prototype or Object.prototype
Monkey-patching built-in prototypes breaks for...in, conflicts with other libraries, and can break future spec additions
Use utility functions, subclasses, or Symbol-keyed methods
Using obj.hasOwnProperty() on arbitrary objects
The object might have its own hasOwnProperty property that shadows the inherited method
Use Object.hasOwn(obj, key) or Object.prototype.hasOwnProperty.call(obj, key)
Thinking property assignment walks the chain
Only reads walk the chain. Writes go directly on the object.
Assignment creates an own property (except when a setter exists on the prototype)
Quiz
What does this code log?
Quiz
If you do rex.bark = function() { return 'Yap!'; }, what happens to Dog.prototype.bark?
Quiz
What is Object.getPrototypeOf(Object.prototype)?
Key Rules
  1. 1__proto__ is the lookup link on every object. .prototype is a property on constructor functions that becomes __proto__ of instances created with new.
  2. 2Property reads walk the prototype chain. Property writes create own properties on the object directly (unless a setter is found on the chain).
  3. 3Object.create(proto) sets the prototype explicitly — use Object.create(null) for safe dictionaries with no inherited properties.
  4. 4Never modify built-in prototypes — it breaks for...in, conflicts with libraries, and can collide with future spec additions.
  5. 5Use Object.hasOwn(obj, key) instead of obj.hasOwnProperty(key) — it's safe against shadowed methods.