Prototype Chain
The prototype chain is the full sequence of linked objects the engine walks through when resolving a property. Every built-in type has its own chain terminating at null.
🎯 What you'll learn
- What the Chain Actually Looks Like
- Property Lookup Algorithm
- Shadowing Along a Multi-Level Chain
- Built-in Chains You Use Every Day
- Walking the Chain Yourself
1What the Chain Actually Looks Like
The prototype chain is the full sequence of linked objects the engine walks through when resolving a property. Every built-in type has its own chain terminating at null.
myArray
└─ Array.prototype (push, map, filter, forEach, ...)
└─ Object.prototype (toString, hasOwnProperty, valueOf, ...)
└─ null (chain ends here)const arr = [1, 2, 3];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // nullThis is why every array can call .push() (from Array.prototype) and .toString() (from Object.prototype, two links up).
2Property Lookup Algorithm
When you access obj.key, the engine performs this exact sequence:
- Does
objhave an own propertykey? If yes, use it — stop. - If not, set
obj = Object.getPrototypeOf(obj). Isobjnownull? If yes, returnundefined— stop. - Otherwise, go back to step 1.
const base = { greeting: "hi" };
const mid = Object.create(base);
const top = Object.create(mid);
top.name = "top-level";
console.log(top.name); // step 1 succeeds immediately — own property
console.log(top.greeting); // step 1 fails on top, fails on mid, succeeds on base
console.log(top.missing); // fails on all three, base's prototype is Object.prototype,
// Object.prototype's prototype is null -> undefined3Shadowing Along a Multi-Level Chain
If more than one object in the chain has the same property name, the first (nearest) match wins — this is called shadowing.
const grandparent = { level: "grandparent" };
const parent = Object.create(grandparent);
parent.level = "parent"; // shadows grandparent.level for parent and anything below it
const child = Object.create(parent);
console.log(child.level); // "parent" — parent is nearer than grandparent4Built-in Chains You Use Every Day
| Value | Chain |
|---|---|
[] (array) | Array.prototype → Object.prototype → null |
function(){} | Function.prototype → Object.prototype → null |
"str" (auto-boxed) | String.prototype → Object.prototype → null |
new Map() | Map.prototype → Object.prototype → null |
class Dog extends Animal {} instance | Dog.prototype → Animal.prototype → Object.prototype → null |
class ... extends is exactly this mechanism — extends sets Dog.prototype's prototype to Animal.prototype, extending the chain by one more link.
5Walking the Chain Yourself
function describeChain(obj) {
const chain = [];
let current = obj;
while (current !== null) {
chain.push(current.constructor?.name ?? "(no constructor)");
current = Object.getPrototypeOf(current);
}
return chain;
}
class Animal {}
class Dog extends Animal {}
console.log(describeChain(new Dog())); // ["Dog", "Animal", "Object"]⚠️ Common Pitfalls
- Assuming the chain is "flat" — it can be arbitrarily deep; each
extendsorObject.createcall adds one more link, and every lookup miss costs a traversal step. for...inwalking the whole chain — it enumerates inherited enumerable properties too, unlikeObject.keys()which only returns own properties. Always guard withhasOwnPropertyif mixing the two matters.- Modifying a prototype deep in the chain at runtime — instantly affects every object below it in the chain that hasn't shadowed that property, even objects created long ago.
- Confusing "own property lookup fails" with "property doesn't exist" — a failed own-property check just means step 2 (go up the chain) triggers; the property might still resolve further up.
Under the HoodWhy Chain Walks Don't Actually Cost O(depth) Every Time▾
Section 2's lookup algorithm — walk up one link, check, repeat — describes the correct observable behavior, but a naive engine that literally re-walked the chain on every single property access would be unacceptably slow for inherited properties (which are looked up far more often than own properties, since methods almost always live on a prototype). Real engines short-circuit this using the hidden-class and inline-cache machinery from the Prototypes topic, extended to cover the whole chain, not just an object's own shape.
Caching an inherited lookup's entire path
When an inline cache records a hit for an inherited property, it doesn't just remember an offset — it remembers which object in the chain actually held the property, tagged to the hidden class of the object the lookup started from (not the object where the property was found). On a cache hit, the engine can jump straight to the known holder without re-walking any intermediate links at all.
function Dog() {}
Dog.prototype.bark = function () { return "woof"; };
function callBark(d) { return d.bark(); } // this call site gets an IC
callBark(new Dog()); // first call: full O(depth) walk — finds bark() on Dog.prototype, caches it
callBark(new Dog()); // second call: SAME hidden class as before -> cache hit, no chain walk at allWhy mutating a prototype at runtime is expensive, not just "risky"
Section 6 of the earlier Prototypes topic warns that changing a shared prototype affects every object below it — the deeper reason this is also a performance cliff, not just a correctness one: adding, removing, or reordering a property on a prototype that's already been cached invalidates every inline cache that pointed at it. Every call site that had cached "found on Dog.prototype, offset N" must fall back to a slow, uncached lookup at least once, re-walk the chain for real, and rebuild its cache from scratch. Frameworks that monkey-patch built-in prototypes (Array.prototype.customMethod = ...) after the application has been running for a while can trigger exactly this kind of cache invalidation across every call site touching that type, application-wide.
hasOwnProperty and in bypass caching differently
"key" in obj and obj.hasOwnProperty("key") are still real chain-walking (or single-shape-check, for hasOwnProperty) operations — they benefit from hidden-class shape checks but are inherently less cacheable than a direct property read, because they're boolean existence checks, not value fetches tied to a single call site's "shape of things I've seen before" history in the same way.
📋 Quick Reference
obj.key lookup:
own property? → use it, stop
→ walk up one link → repeat
→ reached null → undefined🎤 Interview Q&A
Q1How does property lookup traverse the prototype chain?core▾
JS checks the object's own properties, then its prototype, then the prototype's prototype, until Object.prototype and finally null. Writing a property creates it on the object itself (shadowing), not on the prototype.
- Where does
[].mapcome from?Array.prototype.
Q2How does inheritance work with classes under the hood?core▾
class B extends A sets B.prototype.__proto__ = A.prototype (instance methods) and B.__proto__ = A (static methods). super calls look up the parent's prototype.
- Are JS classes real classes? Syntax over prototypes, with some extra semantics.
Q3What does instanceof check?core▾
Whether Constructor.prototype appears anywhere in the object's prototype chain. It can fail across realms (iframes) and can be customized with Symbol.hasInstance.
- Safer array check?
Array.isArray.
Q4Why are long prototype chains or prototype mutation slow?senior▾
Engines optimize objects with stable shapes and prototypes; changing an object's prototype at runtime (Object.setPrototypeOf) deoptimizes code.
- Recommendation? Set prototypes at creation (Object.create, classes).
Q5What's at the end of every prototype chain?core▾
Object.prototype, whose prototype is null, unless the object was created with Object.create(null).
- What methods come from there? toString, hasOwnProperty, valueOf…
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.