Chapter 13

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:

  1. Check the object's own properties
  2. Found โ†’ return
  3. Not found โ†’ get [[Prototype]], repeat step 1 on it
  4. [[Prototype]] is null โ†’ return undefined
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:

  1. 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

  1. 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 or null is reached. The Receiver (this value) stays as the original calling object throughout.

  2. hasOwnProperty checks own properties; in checks the full chain: In daily code, prefer Object.hasOwn(obj, key) (ES2022) over obj.hasOwnProperty(key), as the latter can be overridden.

  3. 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.

  4. instanceof checks the prototype chain, not constructor identity: Replacing F.prototype changes the result for future checks but not for existing instances. Function instanceof Object and Object instanceof Function are both true due to the engine's circular bootstrapping.

  5. Object.create(null) creates objects with no prototype: Ideal as pure data dictionaries, but unable to call any Object.prototype methods โ€” all must be called explicitly.

Rate this chapter
4.7  / 5  (24 ratings)

๐Ÿ’ฌ Comments