The Prototype Chain: Complete [[Prototype]] Lookup Algorithm
The JavaScript prototype chain is not syntactic sugar for "inheritance" โ it is a precisely defined property lookup algorithm. Every time you write obj.method(), the engine runs a concrete recursive process: starting from the object itself, walking up the [[Prototype]] chain until the property is found or null is reached. Understanding this algorithm is what explains why instanceof sometimes gives counterintuitive results, and why mutating __proto__ causes performance disasters.
๐น Level 1 ยท What You Need to Know
Every Object Has [[Prototype]]
Every JavaScript object (except those created with Object.create(null)) has an internal slot called [[Prototype]] pointing to another object or null. This chain is the prototype chain.
// The correct way to access [[Prototype]]
const obj = { x: 1 };
Object.getPrototypeOf(obj); // Object.prototype
// Don't use __proto__ (not in the spec, though all major engines support it)
obj.__proto__; // equivalent, but not recommended
// Create an object with a specified prototype
const child = Object.create(obj);
Object.getPrototypeOf(child) === obj; // true
child.x; // 1 โ inherited from prototype obj
Property Lookup Rules
When looking up a property, the engine follows this order:
- Check the object's own properties
- Found โ return
- Not found โ get
[[Prototype]], repeat step 1 on it [[Prototype]]isnullโ returnundefined
const animal = {
breathe() { return 'breathing'; }
};
const dog = Object.create(animal);
dog.name = 'Rex';
const puppy = Object.create(dog);
// Lookup chain: puppy โ dog โ animal โ Object.prototype โ null
console.log(puppy.name); // 'Rex' (found on dog)
console.log(puppy.breathe()); // 'breathing' (found on animal)
console.log(puppy.fly); // undefined (not found anywhere)
hasOwnProperty vs the in Operator
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;
// hasOwnProperty: only checks own properties
child.hasOwnProperty('own'); // true
child.hasOwnProperty('inherited'); // false
// in operator: checks the entire prototype chain
'own' in child; // true
'inherited' in child; // true
'toString' in child; // true โ comes from Object.prototype
// Object.keys: returns only own enumerable properties
Object.keys(child); // ['own']
// for...in: returns all enumerable properties, including prototype chain
for (const k in child) console.log(k); // 'own', 'inherited'
Practical advice:
// Safe hasOwnProperty call (in case the object overrides it)
Object.prototype.hasOwnProperty.call(obj, key);
// Or use the ES2022 API:
Object.hasOwn(obj, key); // more concise, recommended
Object.create() Use Cases
// 1. Pure data storage (no prototype, avoids prototype chain interference)
const pureMap = Object.create(null);
pureMap.key = 'value';
// No inherited toString, hasOwnProperty, etc.
// Common for JSON-like storage or Map substitutes
// 2. Creating inheritance relationships
const vehicle = {
move() { return `${this.type} is moving at ${this.speed}km/h`; }
};
const car = Object.create(vehicle);
car.type = 'car';
car.speed = 100;
console.log(car.move()); // 'car is moving at 100km/h'
// 3. Setting up inheritance chains (the underlying mechanism of class)
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// Critical: set up the prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // fix the constructor pointer
Dog.prototype.bark = function() {
return `${this.name} barks`;
};
const rex = new Dog('Rex', 'Labrador');
console.log(rex.speak()); // 'Rex makes a sound'
console.log(rex.bark()); // 'Rex barks'
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
5 Common Mistakes
Mistake 1: Not knowing Object.keys skips prototype properties
function Person(name) { this.name = name; }
Person.prototype.greet = function() {};
const p = new Person('Alice');
Object.keys(p); // ['name'] โ own properties only
JSON.stringify(p); // '{"name":"Alice"}' โ same, own enumerable only
Mistake 2: Mutating a prototype affects all instances
function Cat(name) { this.name = name; }
Cat.prototype.sound = 'meow';
const c1 = new Cat('Tom');
const c2 = new Cat('Jerry');
Cat.prototype.sound = 'purr';
console.log(c1.sound); // 'purr'
console.log(c2.sound); // 'purr'
// Setting an instance property shadows the prototype
c1.sound = 'hiss';
console.log(c1.sound); // 'hiss' (own property, shadows prototype)
console.log(c2.sound); // 'purr' (still reads from prototype)
Mistake 3: Thinking class extends copies methods
class A {
method() { return 'A'; }
}
class B extends A {}
const b = new B();
b.method(); // 'A'
// Nothing is copied โ B.prototype's [[Prototype]] points to A.prototype
Object.getPrototypeOf(B.prototype) === A.prototype; // true
// Modifying A.prototype still affects B instances
A.prototype.method = function() { return 'A modified'; };
b.method(); // 'A modified'
Mistake 4: Misusing instanceof
function Foo() {}
const f = new Foo();
// Replacing the prototype after the fact
Foo.prototype = {};
console.log(f instanceof Foo); // false! โ f's [[Prototype]] is still the old one
Mistake 5: Object.create(null) objects can't call prototype methods
const safe = Object.create(null);
safe.key = 'value';
safe.toString(); // TypeError: safe.toString is not a function
safe.hasOwnProperty('key'); // TypeError
// Must call explicitly:
Object.prototype.toString.call(safe); // '[object Object]'
Object.hasOwn(safe, 'key'); // true (ES2022)
๐ธ Level 2 ยท How It Works Internally
The Complete Prototype Chain Structure
Complete prototype chain structure (plain objects and functions):
obj = { x: 1 }
โ
โ [[Prototype]]
โผ
Object.prototype โ end of chain for plain objects
โ constructor: Object
โ toString: function
โ hasOwnProperty: function
โ valueOf: function
โ ...
โ
โ [[Prototype]]
โผ
null โ absolute end of prototype chain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function foo() {}
โ
โ [[Prototype]] (function object's prototype chain)
โผ
Function.prototype โ prototype of all functions
โ call: function
โ apply: function
โ bind: function
โ ...
โ
โ [[Prototype]]
โผ
Object.prototype
โ
โ [[Prototype]]
โผ
null
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const arr = [1, 2, 3]
โ
โ [[Prototype]]
โผ
Array.prototype โ prototype of array instances
โ push, pop, map, filter, ...
โ
โ [[Prototype]]
โผ
Object.prototype
โ
โ [[Prototype]]
โผ
null
The Complete [[Get]] Operation Steps
When you write obj.x, the engine executes obj.[[Get]]('x', obj):
OrdinaryGet(O, P, Receiver) execution:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ 1. desc โ O.[[GetOwnProperty]](P) โ
โ โ โ
โ โโโ desc is not undefined (own property found) โ
โ โ โโโ IsDataDescriptor(desc) โ
โ โ โ โโโ return desc.[[Value]] โ
โ โ โโโ IsAccessorDescriptor(desc) โ
โ โ โโโ desc.[[Get]] is undefined โ return undefined โ
โ โ โโโ Call(desc.[[Get]], Receiver) โ
โ โ โโโ return getter's return value โ
โ โ โ
โ โโโ desc is undefined (no own property) โ
โ โ โ
โ โผ โ
โ 2. parent โ O.[[GetPrototypeOf]]() โ
โ โ โ
โ โโโ parent is null โ return undefined โ
โ โ โ
โ โโโ parent is not null โ
โ โโโ recursively call parent.[[Get]](P, Receiver) โ
โ โโโ repeat steps 1-2 until found or null โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Critical detail: The Receiver parameter (the this value) remains unchanged throughout the recursion. This is why methods found on the prototype chain still have this pointing to the original calling object:
const proto = {
getThis() { return this; }
};
const child = Object.create(proto);
child.getThis() === child; // true โ even though the method is on proto
V8 Inline Cache Optimization and Chain Depth
V8 uses Inline Caches (IC) to accelerate property lookups. On first access, V8 records the lookup path; subsequent accesses with the same object shape (HiddenClass) skip straight to the cached result.
V8 property access performance tiers:
Fast path (fastest):
โโโ Object has fixed shape (HiddenClass), property is own โ direct memory offset
IC hit (fast):
โโโ Shape matches cached entry โ no prototype chain traversal needed
IC polymorphic (medium):
โโโ 2-4 shapes at this site โ IC stores multiple paths
IC megamorphic (slow):
โโโ More than 4 shapes โ IC gives up, falls back to generic lookup
Chain depth impact:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ Prototype chain depth โ Relative cost โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโค
โ Own property (depth 0) โ 1x (baseline) โ
โ depth 1 (direct prototype) โ ~1.1x โ
โ depth 3 โ ~1.5x โ
โ depth 6 โ ~2.5x โ
โ depth 10+ โ ~4x+ (notable) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโ
function createDeepChain(depth) {
let obj = { value: 'found' };
for (let i = 0; i < depth; i++) {
obj = Object.create(obj);
}
return obj;
}
const shallow = createDeepChain(0); // own property
const deep6 = createDeepChain(6); // depth 6
// In real benchmarks, deep6 accesses are roughly 40-60% the speed of shallow
// Exact numbers vary by V8 version and object shape
Object.create(null) Internals
const noproto = Object.create(null);
// Creates an object where [[Prototype]] is null
// Internal: { [[Prototype]]: null, ... }
Object.getPrototypeOf(noproto); // null
noproto instanceof Object; // false โ Object.prototype is not on the chain
// Use case: high-performance dictionary / Map substitute
// Benefit: no need to check if prototype methods are shadowed
const dict = Object.create(null);
dict['toString'] = 'my value'; // no conflict with Object.prototype.toString
The Dual Nature of Function Prototypes
Function objects have two prototype-related properties that are often confused:
A function's two prototype-related properties:
function Foo() {}
โ
โ [[Prototype]] (the prototype of the function object itself)
โผ
Function.prototype (all functions live on this chain)
โ
โ [[Prototype]]
โผ
Object.prototype
โ
โผ
null
Foo.prototype (the [[Prototype]] of objects created with new Foo())
โ โ
โ This is a DIFFERENT thing!
โ
โโโ Foo.prototype.[[Prototype]] === Object.prototype
function Foo() {}
// [[Prototype]]: the prototype of the Foo function object (Foo is an instance of Function)
Object.getPrototypeOf(Foo) === Function.prototype; // true
// Foo.prototype: when you do new Foo(), the instance's [[Prototype]] is set to this
const instance = new Foo();
Object.getPrototypeOf(instance) === Foo.prototype; // true
๐บ Level 3 ยท How the Spec Defines It
Spec ยง10.1.8 [[GetPrototypeOf]]
Spec text (ยง10.1.8):
10.1.8 OrdinaryGetPrototypeOf ( O )
The abstract operation OrdinaryGetPrototypeOf takes argument O (an Object) and returns an Object or null. It performs the following steps when called:
- Return O.[[Prototype]].
This is one of the simplest spec operations โ it directly returns the internal slot value.
The Complete OrdinaryGet Lookup Algorithm
Spec ยง10.1.8 OrdinaryGet(O, P, Receiver) โ the core algorithm of prototype chain lookup:
Spec text (simplified, key logic preserved):
OrdinaryGet(O, P, Receiver):
1. desc โ OrdinaryGetOwnProperty(O, P)
(check if O has own property P)
2. If desc is undefined:
a. parent โ OrdinaryGetPrototypeOf(O)
(get O's [[Prototype]])
b. If parent is null:
return undefined
c. Return parent.[[Get]](P, Receiver)
(recurse: look up in parent, Receiver stays the same)
3. If IsDataDescriptor(desc):
return desc.[[Value]]
4. Assert: IsAccessorDescriptor(desc)
getter โ desc.[[Get]]
5. If getter is undefined:
return undefined
6. Return Call(getter, Receiver)
(call getter with this = Receiver)
The role of Receiver: Receiver is the this value of the property access expression. Even if the property is deep in the prototype chain, this remains the original calling object. This is why polymorphic methods work correctly:
class Shape {
area() { return this.width * this.height; } // this refers to concrete instance
}
class Rectangle extends Shape {
constructor(w, h) {
super();
this.width = w;
this.height = h;
}
}
const r = new Rectangle(4, 5);
r.area(); // 20 โ area is on Shape.prototype, but this is r
Invariants on [[GetPrototypeOf]]
The spec (ยง10.1.8) imposes invariants on [[GetPrototypeOf]]:
6.1.7.3 Invariants of the Essential Internal Methods
[[GetPrototypeOf]] ()
- The Type of the return value must be either Object or Null.
- If O is not extensible, [[GetPrototypeOf]] must always return the same value.
This invariant explains why a Proxy cannot lie about the prototype of a non-extensible object โ violating invariants causes a TypeError at the Proxy layer (see ch15).
The instanceof Spec Implementation
x instanceof F does not check whether x was constructed by F โ it checks the prototype chain:
Spec ยง13.10.2 InstanceofOperator(V, target):
1. If target is not an object โ TypeError
2. instOfHandler โ GetMethod(target, @@hasInstance)
(check if target[Symbol.hasInstance] exists)
3. If instOfHandler is not undefined:
return ToBoolean(Call(instOfHandler, target, [V]))
(allows custom instanceof behavior)
4. If IsCallable(target) is false โ TypeError
5. Return OrdinaryHasInstance(target, V)
OrdinaryHasInstance(C, O):
1. If IsCallable(C) is false โ false
2. If C has [[BoundTargetFunction]] โ recurse with bound target
3. If Type(O) is not Object โ false
4. P โ C.prototype
If P is not an object โ TypeError
5. Loop:
O โ O.[[GetPrototypeOf]]()
If O is null โ false
If SameValue(O, P) is true โ true
(continue loop, walk up the prototype chain)
// Proof: instanceof checks the chain, not constructor identity
function Foo() {}
const f = new Foo();
Foo.prototype = {}; // replace with a new object
console.log(f instanceof Foo); // false!
// f.[[Prototype]] still points to the old Foo.prototype
// SameValue comparison against new Foo.prototype fails
// Custom instanceof behavior:
class EvenNumber {
static [Symbol.hasInstance](num) {
return Number.isInteger(num) && num % 2 === 0;
}
}
console.log(2 instanceof EvenNumber); // true
console.log(3 instanceof EvenNumber); // false
console.log(4.5 instanceof EvenNumber); // false
๐ Level 4 ยท Edge Cases and Traps
Trap 1: Function.prototype Is Itself an Object
This is the most confusing circular reference in JavaScript's prototype system:
// Function.prototype is a function, but also an object
typeof Function.prototype; // 'function' โ it's callable
Function.prototype(); // returns undefined (doesn't throw)
// Its [[Prototype]] is Object.prototype (not Function.prototype itself)
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
// The full circular dependency:
Object.getPrototypeOf(Function) === Function.prototype; // true (Function is its own instance)
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object) === Function.prototype; // true (Object is also a function)
Object.getPrototypeOf(Object.prototype) === null; // true (end of chain)
The circular dependency visualized:
Circular prototype relationship:
Function โโ[[Prototype]]โโโถ Function.prototype โโ[[Prototype]]โโโถ Object.prototype โโโถ null
โฒ โ
โ โ (Function.prototype is an instance of Object)
โ โผ
Object โโ[[Prototype]]โโโถ Function.prototype (same object)
โ
โ Object.prototype โโ[[Prototype]]โโโถ null
// Verify all relationships:
console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Function); // true (Function is its own instance)
Trap 2: Object.prototype.proto === null
Object.getPrototypeOf(Object.prototype); // null
Object.prototype.__proto__; // null
// Note: __proto__ on Object.prototype is an accessor property
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {
// get: [Function: get __proto__],
// set: [Function: set __proto__],
// enumerable: false,
// configurable: true
// }
// __proto__ getter โก Object.getPrototypeOf
// __proto__ setter โก Object.setPrototypeOf
Trap 3: Full Derivation of the instanceof Circular Paradox
A common advanced interview question:
Function instanceof Object // true or false?
Object instanceof Function // true or false?
Full derivation:
// Function instanceof Object:
// Question: Is Object.prototype on Function's [[Prototype]] chain?
//
// Function.[[Prototype]] = Function.prototype
// Function.prototype.[[Prototype]] = Object.prototype โ HIT!
// Conclusion: Function instanceof Object === true
// Object instanceof Function:
// Question: Is Function.prototype on Object's [[Prototype]] chain?
//
// Object.[[Prototype]] = Function.prototype โ direct hit!
// (Object is a function; all functions have [[Prototype]] = Function.prototype)
// Conclusion: Object instanceof Function === true
Both are true because JavaScript's prototype system is bootstrapped โ Function and Object are created specially during engine initialization, forming this circular dependency. User code cannot create this kind of cycle.
Trap 4: The Performance Penalty of Mutating proto
const obj = { x: 1 };
// This line causes serious performance issues:
obj.__proto__ = { y: 2 };
// Equivalently:
Object.setPrototypeOf(obj, { y: 2 });
Why is it costly?
V8 maintains a HiddenClass (Shape) for each object to quickly determine property memory offsets. When you change an object's [[Prototype]]:
Cost of mutating [[Prototype]]:
1. Invalidates the current object's HiddenClass
โโโ All Inline Caches depending on this HiddenClass are invalidated
2. If the object is a local variable inside a function,
V8 may deoptimize the entire JIT-compiled function
3. Scope of impact: not just this one line, but all code paths
that have ever accessed this object
Measured impact (V8 8.x):
Normal property access: ~1ns
Property access after [[Prototype]] change: ~10-50ns (initial, waiting for re-opt)
Frequent [[Prototype]] changes in hot loop: >10x slowdown possible
// Bad pattern: mutating prototype in a loop
function process(items) {
items.forEach(item => {
Object.setPrototypeOf(item, specialProto); // invalidates IC every time!
});
}
// Good pattern: set up the prototype at creation time
function createItem(data) {
return Object.assign(Object.create(specialProto), data);
}
The Node.js documentation specifically warns:
"Changing the prototype of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-reaching, and are not limited to simply the time spent in
obj.__proto__ = ...statement."
Trap 5: Object.create(null) Objects and JSON.stringify
const noproto = Object.create(null);
noproto.name = 'Alice';
noproto.age = 30;
// JSON.stringify does not depend on Object.prototype
JSON.stringify(noproto); // '{"name":"Alice","age":30}' โ works fine!
// Why? JSON.stringify internally uses [[OwnPropertyKeys]] and [[Get]],
// which are internal methods that don't depend on the prototype chain
// toJSON works too if defined as own method:
noproto.toJSON = function() { return { custom: true }; };
JSON.stringify(noproto); // '{"custom":true}'
// What breaks: anything that calls inherited Object.prototype methods
noproto.toString(); // TypeError โ no toString method
noproto.hasOwnProperty('name'); // TypeError
// Detecting Object.create(null) objects:
Object.getPrototypeOf(noproto) === null; // true
// Type-check helper:
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false;
const proto = Object.getPrototypeOf(obj);
return proto === null || proto === Object.prototype;
}
Real-world bug: Using Object.create(null) as a cache and later calling cache.hasOwnProperty(key) or some utility that calls .toString() on the object. Fix: always use Object.hasOwn(cache, key) (ES2022) or Object.prototype.hasOwnProperty.call(cache, key).
Prototype Chain and Memory Leaks
The prototype chain itself doesn't cause memory leaks, but certain patterns do:
// Dangerous: storing large data on a prototype
function createHeavyProto() {
const largeData = new Array(1000000).fill('data');
return { largeData };
}
const heavyProto = createHeavyProto();
// All objects using heavyProto as prototype hold a reference to largeData
const obj1 = Object.create(heavyProto);
const obj2 = Object.create(heavyProto);
// largeData won't be GC'd until heavyProto itself has no references
// Correct pattern: share methods, not large data
const proto = {
greet() { return `Hello, ${this.name}`; } // small, fine
};
const obj = Object.create(proto);
obj.name = 'Alice'; // data lives on the instance
Chapter Summary
-
The prototype chain is a precise recursive lookup algorithm:
[[Get]]starts from the current object, walks up[[Prototype]]step by step until the property is found ornullis reached. The Receiver (thisvalue) stays as the original calling object throughout. -
hasOwnPropertychecks own properties;inchecks the full chain: In daily code, preferObject.hasOwn(obj, key)(ES2022) overobj.hasOwnProperty(key), as the latter can be overridden. -
Mutating
[[Prototype]]is extremely costly: V8 invalidates all related Inline Caches and JIT-compiled code. Always set up the prototype at creation time, never after the fact. -
instanceofchecks the prototype chain, not constructor identity: ReplacingF.prototypechanges the result for future checks but not for existing instances.Function instanceof ObjectandObject instanceof Functionare bothtruedue to the engine's circular bootstrapping. -
Object.create(null)creates objects with no prototype: Ideal as pure data dictionaries, but unable to call anyObject.prototypemethods โ all must be called explicitly.