๐Ÿณ IntuitiveFE
Login
โ† All concepts

Prototype Chain

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ”— Every object has a secret pointer โ€” [[Prototype]] โ€” to its parent blueprint. Property lookup climbs this chain until it finds the property or hits null.

The blueprint analogy

Imagine a contractor who doesn't have every tool in their toolbox โ€” they have a toolbox (own properties) plus a secret pointer to a shared workshop (the prototype). If they don't have a tool themselves, they walk next door to the workshop. If the workshop doesn't have it, the workshop has its own pointer to an even bigger supply depot. This chain continues until someone has the tool or you reach a location with no next pointer (null).

JavaScript stores this pointer in an internal slot called [[Prototype]]. Every object has exactly one โ€” either pointing to another object or to null (end of chain).

Lookup algorithm โ€” ES spec ยง10.1.8.1 OrdinaryGet

js
1// Pseudocode of what the engine does when you access obj.prop
2function OrdinaryGet(obj, prop) {
3 // 1. Check own properties first
4 const ownDesc = Object.getOwnPropertyDescriptor(obj, prop);
5 if (ownDesc !== undefined) return ownDesc.value;
6ย 
7 // 2. Follow [[Prototype]]
8 const parent = Object.getPrototypeOf(obj);
9 if (parent === null) return undefined; // end of chain
10ย 
11 // 3. Recurse up the chain
12 return OrdinaryGet(parent, prop);
13}

This recursive walk happens every time you access a property via dot or bracket notation. The engine checks own properties first, then climbs one link at a time.

Three confusing accessors โ€” memorize the difference

obj.__proto__

Legacy instance accessor โ€” reads/writes [[Prototype]] of obj. Works in browsers but not in null-proto objects. Deprecated. Never use in production.

Object.getPrototypeOf(obj)

The standard, spec-compliant way to read [[Prototype]]. Works on all objects including null-proto ones. Always prefer this.

Foo.prototype

Completely different thing! This is the object that new Foo() instances will have as their [[Prototype]]. It lives on the function object, not on instances. Confusing same word, different concept.

js
1function Dog() {}
2const d = new Dog();
3// d's [[Prototype]] === Dog.prototype โœ“ (instance's parent)
4// Dog's [[Prototype]] === Function.prototype (Dog is a function)

class syntax is prototype sugar

class syntax

js
1class Animal {
2 speak() { return "..."; }
3}
4class Dog extends Animal {
5 bark() { return "Woof!"; }
6}
7const d = new Dog();

desugared prototype assignments

ts
1// Engine does exactly this:
2Animal.prototype.speak = function() {
3 return "...";
4};
5Dog.prototype = Object.create(
6 Animal.prototype
7);
8Dog.prototype.constructor = Dog;
9Dog.prototype.bark = function() {
10 return "Woof!";
11};

class introduces no new mechanism. It is syntactic sugar over prototype chain assignment โ€” the same chain that exists for arrays, functions, and plain objects.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Object type

Code

js
1const obj = { a: 1, b: 2 };
2obj.toString(); // found at Object.prototype

[[Prototype]] chain

obj
instance
a: 1b: 2

Your object โ€” holds own properties only

โ†ณ no own ยซtoStringยป โ€” follow [[Prototype]]

[[Prototype]]
โ†“
Object.prototype
FOUND HEREroot
toString()hasOwnProperty()valueOf()keys()isPrototypeOf()

Every plain object's prototype โ€” the root of most chains

โ†ณ ยซtoStringยป found here โœ“

[[Prototype]]
โ†“
null
null

End of chain โ€” nothing above Object.prototype

Lookup trace โ€” ยซtoStringยป

1.obj.toString โ€” undefined, climb...
2.Object.prototype.toString โœ“ โ€” found!

hasOwnProperty vs in

own:obj.hasOwnProperty('a') โ†’ true
in: 'toString' in obj โ†’ true (at chain[1])
hasOwnProperty โ€” only own properties
in operator โ€” walks entire prototype chain
Gotcha: Every object literal inherits from Object.prototype. Calling obj.toString() walks up one level automatically.
Insight: Plain objects sit one hop above null. Object.prototype is the universal ancestor for all non-null-proto objects.
Explored:๐Ÿ“ฆ๐Ÿ“‹โš™๏ธ๐Ÿ•๐Ÿšซ
๐ŸŽฏ

Challenge

Explore all 5 object types and observe where property lookup resolves in each chain.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: Difference between obj.hasOwnProperty('x') and 'x' in obj?

js
1const obj = Object.create({ inherited: true });
2obj.own = true;
3ย 
4obj.hasOwnProperty('own') // true โ€” checks only obj's own props
5obj.hasOwnProperty('inherited') // false โ€” inherited, not own
6obj.hasOwnProperty('toString') // false โ€” from Object.prototype
7ย 
8'own' in obj // true
9'inherited' in obj // true โ€” walks the chain!
10'toString' in obj // true โ€” found at Object.prototype!

Rule: hasOwnProperty never climbs. in always climbs. Use hasOwnProperty when iterating โ€” otherwise you may accidentally process inherited enumerable properties.

Q: How does instanceof work?

js
1// Pseudocode โ€” what the engine does
2function instanceof(obj, Constructor) {
3 let proto = Object.getPrototypeOf(obj);
4 while (proto !== null) {
5 if (proto === Constructor.prototype) return true;
6 proto = Object.getPrototypeOf(proto); // climb
7 }
8 return false;
9}
10ย 
11const d = new Dog();
12d instanceof Dog // true โ€” Dog.prototype in d's chain
13d instanceof Animal // true โ€” Animal.prototype in d's chain
14d instanceof Array // false โ€” not in d's chain

instanceof literally walks the [[Prototype]] chain checking if Constructor.prototype appears anywhere. It is NOT about the constructor function โ€” you can fool it by swapping Constructor.prototype.

Q: Why can you add to Array.prototype and all arrays get it? Risk?

js
1// All arrays share ONE Array.prototype object
2Array.prototype.sum = function() {
3 return this.reduce((a, b) => a + b, 0);
4};
5[1, 2, 3].sum(); // 6 โ€” every array gets this immediately
6ย 
7// THE RISK: for...in loop iterates ALL enumerable props
8const arr = [1, 2, 3];
9for (const key in arr) {
10 console.log(key); // "0", "1", "2", "sum" โ† leaked!
11}
12// Fix:
13for (const key in arr) {
14 if (arr.hasOwnProperty(key)) console.log(key); // "0", "1", "2" only
15}

This is called monkey-patching. It's how older polyfills worked. Avoid in modern code โ€” it breaks for...in loops and risks collisions with future native methods.

Production bugs

Bug 1: for...in leaks prototype chain

js
1// โœ— WRONG โ€” iterates prototype keys too
2const obj = Object.create({ base: true });
3obj.a = 1;
4obj.b = 2;
5for (const key in obj) {
6 data[key] = obj[key]; // picks up "base" from prototype!
7}
8ย 
9// โœ“ FIX โ€” guard with hasOwnProperty
10for (const key in obj) {
11 if (Object.prototype.hasOwnProperty.call(obj, key)) {
12 data[key] = obj[key]; // only own keys
13 }
14}
15// Or just use Object.keys() โ€” returns only own enumerable props
16Object.keys(obj).forEach(key => { data[key] = obj[key]; });

Bug 2: Object.create(null) breaks toString

js
1// Common in hash map pattern
2const cache = Object.create(null);
3cache.key1 = "value";
4ย 
5// Works fine for storage:
6cache['key1'] // "value" โœ“
7ย 
8// Breaks anything using Object.prototype methods:
9cache.toString() // TypeError: not a function!
10cache.hasOwnProperty // undefined!
11JSON.stringify(cache) // OK โ€” JSON uses its own path
12ย 
13// Fix: explicitly call from Object.prototype when needed:
14Object.prototype.hasOwnProperty.call(cache, 'key1') // true โœ“
15String(cache) // "[object Object]" via explicit conversion
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

ES spec ยง10.1 โ€” OrdinaryObject internal methods

Every JavaScript object has a set of internal methods defined by the spec โ€” denoted with double brackets like [[Get]], [[Set]], [[HasProperty]]. These are not accessible from code โ€” they describe the engine's behavior.

[[Get]](P, Receiver) is what runs when you write obj.prop. The spec algorithm: (1) get own property descriptor; (2) if undefined, get parent via [[GetPrototypeOf]]; (3) if parent is null, return undefined; (4) call parent.[[Get]](P, Receiver) recursively. The chain walk is recursive delegation between internal method implementations.

js
1// Proxy lets you intercept these internal methods
2const traced = new Proxy(obj, {
3 get(target, prop, receiver) {
4 console.log(`[[Get]] called for: ${String(prop)}`);
5 return Reflect.get(target, prop, receiver); // default [[Get]]
6 },
7 getPrototypeOf(target) {
8 console.log("[[GetPrototypeOf]] called");
9 return Reflect.getPrototypeOf(target);
10 },
11});

Property shadowing

When an own property has the same name as a prototype property, the own property shadows the prototype one. The chain walk stops at the first match.

js
1const parent = { greet: () => "Hello from parent" };
2const child = Object.create(parent);
3child.greet = () => "Hello from child"; // shadows parent.greet
4ย 
5child.greet() // "Hello from child" โ† own property wins
6parent.greet() // "Hello from parent" โ† parent unaffected
7ย 
8// Dangerous: accidentally shadowing builtins
9const obj = { hasOwnProperty: () => false };
10obj.hasOwnProperty('x') // always false! own prop shadows built-in
11// Safe call:
12Object.prototype.hasOwnProperty.call(obj, 'x') // correct

Performance โ€” chain length and V8 hidden classes

Each [[Prototype]] hop is a hashtable lookup. Long chains โ€” 5+ links โ€” cause measurable slowdowns in tight loops. V8 uses hidden classes (also called "shapes" or "maps") and inline caches to optimize repeat property accesses. If the shape of an object changes (e.g., properties added in different order), V8 deoptimizes and falls back to slower lookup.

Avoid in hot paths: Deeply nested prototype chains (e.g., 8-level class hierarchies) are slower than shallow composition patterns like mixins. For performance-critical code, prefer flat objects or shallow chains.

Three ways to set [[Prototype]]

Object.create(proto)

js
1const child = Object.create(parent);
2// child's [[Prototype]] === parent
3// No constructor involved โ€” pure prototype linking

Cleanest way. Explicit, no side effects.

new Constructor()

js
1function Dog() {}
2const d = new Dog();
3// d's [[Prototype]] === Dog.prototype
4// new creates a fresh object, sets [[Prototype]] to Dog.prototype,
5// then calls Dog() with 'this' = the new object

Classic constructor pattern. class is sugar over this.

class extends

ts
1class Dog extends Animal {}
2// Engine sets: Dog.prototype = Object.create(Animal.prototype)
3// Plus wires up constructor and static inheritance

Syntactic sugar โ€” most readable, recommended for class hierarchies.

Object.setPrototypeOf โ€” use with extreme caution

js
1const obj = {};
2Object.setPrototypeOf(obj, Array.prototype); // โ† live mutation!
3ย 
4// MDN warning verbatim:
5// "Mutating the [[Prototype]] of an object is, by the nature
6// of how modern JavaScript engines optimize property accesses,
7// currently a very slow operation in every browser and JS engine."
8//
9// It invalidates ALL V8 inline caches for every object that
10// ever had obj in its prototype chain โ€” not just obj itself.
11// Use Object.create() at construction time instead.
Rule: Set [[Prototype]] once at creation time via Object.create() or class extends. Never mutate an existing object's prototype chain with setPrototypeOf in production.

Mixin pattern โ€” sharing without extending the chain

Instead of adding a long inheritance chain, copy methods directly onto the target prototype. No extra chain links โ€” faster lookup, no diamond inheritance issues.

js
1const Serializable = {
2 serialize() { return JSON.stringify(this); },
3 deserialize(json) { return Object.assign(this, JSON.parse(json)); },
4};
5const Validatable = {
6 validate() { return Object.keys(this).every(k => this[k] != null); },
7};
8ย 
9// Mix in without extending the chain:
10class User {}
11Object.assign(User.prototype, Serializable, Validatable);
12ย 
13const u = new User();
14u.serialize() // works โ€” now on User.prototype directly
15u.validate() // works โ€” same
16ย 
17// Chain stays short: u โ†’ User.prototype โ†’ Object.prototype โ†’ null
18// vs class hierarchy: u โ†’ User.prototype โ†’ Serializable.prototype
19// โ†’ Validatable.prototype โ†’ Object.prototype โ†’ null (slower)

React class components used this pattern internally. Modern composition patterns (hooks, composables) go further โ€” they avoid the prototype chain entirely by closing over shared state in plain functions.

๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

`hasOwnProperty` checks only own properties; `in` walks the entire prototype chain.

`instanceof` walks `obj`'s [[Prototype]] chain checking if `Constructor.prototype` appears anywhere.

An object with no [[Prototype]] โ€” no inherited keys, no `toString`, no `hasOwnProperty`.

An own property with the same name as a prototype property silently shadows it โ€” including inherited builtins like `hasOwnProperty` itself.

Mutating a live object's [[Prototype]] invalidates V8's inline caches for every object in that chain, not just the mutated object.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

Which is the safest way to check if an object has an inherited property named `toString`?