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 user → Object.prototype → null. 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:
- Checks whether
objhas an own property namedprop. - If not, follows
[[Prototype]]to the next object and checks there. - Repeats until it finds the property or reaches
null(yieldingundefined).
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 dog → Animal.prototype → Object.prototype → null. 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.prototypeorObject.prototypeaffects every object of that type in the entire program. Avoid it — it causes hard-to-trace bugs and breaksfor...inloops 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
| API | Purpose |
|---|---|
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 obj | True 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.getPrototypeOfover the legacy__proto__accessor for reading the chain. - Never reassign
Object.setPrototypeOfon 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)(orObject.prototype.hasOwnProperty.call) instead of callingobj.hasOwnPropertydirectly, which breaks onObject.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.