Skip to content
JavaScript js prototypes 4 min read

The Prototype Chain

Every object in JavaScript carries a hidden link to another object called its prototype. When you read a property that an object doesn’t own directly, the engine walks this series of links — the prototype chain — until it finds the property or runs out of links. Understanding this lookup is the key to understanding how inheritance, shared methods, and method shadowing actually work under the hood.

What the chain is

Each object has an internal slot, written [[Prototype]] in the spec, that points to either another object or null. You can read this link with Object.getPrototypeOf(obj) (the modern API) or the legacy __proto__ accessor. Chaining these links together forms a linked list that always ends at null.

A typical object literal sits one step below Object.prototype:

const user = { name: "Ada" };

console.log(Object.getPrototypeOf(user) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype));          // null

Output:

true
null

So userObject.prototypenull. That is the shortest possible chain for a plain object.

How lookup walks the chain

Property access is resolved dynamically every time. When you write obj.prop, the engine:

  1. Checks whether obj has an own property named prop.
  2. If not, follows [[Prototype]] to the next object and checks there.
  3. Repeats until it finds the property or reaches null (yielding undefined).

Methods defined on a constructor’s prototype are found this way, which is why thousands of instances can share a single function in memory:

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  return `${this.name} makes a sound`;
};

const dog = new Animal("Rex");
console.log(dog.speak());                              // own? no -> Animal.prototype
console.log(dog.hasOwnProperty("speak"));             // false
console.log("speak" in dog);                          // true (inherited counts)

Output:

Rex makes a sound
false
true

Here the full chain is dogAnimal.prototypeObject.prototypenull. The name property is found on step one; speak on step two; hasOwnProperty on step three.

Visualizing the chain

        dog (instance)
        ├─ name: "Rex"          (own property)

        ▼  [[Prototype]]
        Animal.prototype
        ├─ speak: ƒ             (shared method)
        ├─ constructor: Animal

        ▼  [[Prototype]]
        Object.prototype
        ├─ hasOwnProperty: ƒ
        ├─ toString: ƒ

        ▼  [[Prototype]]
        null                    (end of chain)

Reading dog.toString() walks three links down to Object.prototype before it succeeds.

Shared methods, single copy

Because lookup resolves through the prototype, every instance points at the same function object. This is both a memory win and a correctness guarantee — patch the prototype and all existing instances see the change instantly.

const a = new Animal("Milo");
const b = new Animal("Otis");

console.log(a.speak === b.speak); // true — one shared function

Animal.prototype.speak = function () {
  return `${this.name} barks`;
};

console.log(a.speak()); // reflects the live update

Output:

true
Milo barks

Mutating a built-in prototype like Array.prototype or Object.prototype affects every object of that type in the entire program. Avoid it — it causes hard-to-trace bugs and breaks for...in loops across libraries.

Shadowing

When an object has its own property with the same name as one further up the chain, the own property wins and the inherited one is never reached. This is called shadowing (or overriding at the instance level). Assignment always writes to the object itself — it never modifies the prototype.

const cat = new Animal("Whiskers");

cat.speak = function () {
  return `${this.name} meows`; // shadows Animal.prototype.speak
};

console.log(cat.speak());                                  // own wins
console.log(new Animal("Tom").speak());                   // prototype version
console.log(Object.getPrototypeOf(cat).speak.call(cat));  // reach the shadowed one

Output:

Whiskers meows
Tom barks
Whiskers barks

Notice the assignment cat.speak = ... created an own property; it did not touch Animal.prototype. To still call the inherited version you must reach it explicitly through the prototype.

Useful inspection APIs

APIPurpose
Object.getPrototypeOf(obj)Read an object’s [[Prototype]] (preferred).
Object.setPrototypeOf(obj, proto)Set the link (slow — avoid in hot paths).
obj.hasOwnProperty(key)True only for own properties, not inherited.
Object.hasOwn(obj, key)ES2022 safer alternative that works on null-prototype objects.
key in objTrue for own or inherited enumerable/non-enumerable keys.
proto.isPrototypeOf(obj)True if proto appears anywhere in obj’s chain.
console.log(Animal.prototype.isPrototypeOf(dog)); // true
console.log(Object.hasOwn(dog, "name"));          // true
console.log(Object.hasOwn(dog, "speak"));         // false

Output:

true
true
false

Best practices

  • Prefer Object.getPrototypeOf over the legacy __proto__ accessor for reading the chain.
  • Never reassign Object.setPrototypeOf on a live object in performance-sensitive code — it deoptimizes the engine; build the object with the right prototype from the start.
  • Keep chains short and shallow; deep chains slow lookups and make debugging harder.
  • Use Object.hasOwn(obj, key) (or Object.prototype.hasOwnProperty.call) instead of calling obj.hasOwnProperty directly, which breaks on Object.create(null) objects.
  • Never mutate built-in prototypes (Array.prototype, Object.prototype); use helper functions or subclasses instead.
  • Remember that assignment writes own properties — to update behavior for all instances, modify the prototype, not an instance.
Last updated June 1, 2026
Was this helpful?