Field Guide · Vol. 4
[[Prototype]] → The Chain

JavaScript Prototypes, End to End.

JavaScript has no classes. Not really. The class keyword is theatre — a polite syntactic mask over the same mechanism that's powered the language since 1995: objects linking to other objects.

01 · The Core Idea

Every object has a parent

In JavaScript, every object has a hidden internal slot called [[Prototype]] — a pointer to another object. When you ask for a property, the engine looks at the object itself, and if it doesn't find it, follows that pointer. And follows the next. Until either it finds the property, or hits null.

dog name: 'Rex' breed: 'Husky' [[Prototype]] ──→ Animal.prototype eat() { ... } sleep() { ... } [[Prototype]] ──→ Object.prototype toString() hasOwnProperty() [[Prototype]] ──→ null (end of chain)

Fig. 1 — When you call dog.toString(), the engine walks dog → Animal.prototype → Object.prototype until it finds it.

The four names for the same thing

[[Prototype]]

Internal slot. Every object has one. Points to another object (or null). The thing the engine actually walks.

__proto__

Legacy getter/setter exposing [[Prototype]]. Works in all browsers but deprecated.

.prototype

A property on functions only. It's the object that becomes [[Prototype]] of instances created with new.

Object.getPrototypeOf(obj)

The modern, correct way to read an object's prototype.

02 · The Mechanics

What happens when you declare them

Function and class declarations both create constructor functions. Class adds ergonomics and subtle behavior — but the underlying object structure is identical.

When you declare a function

function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function() {
  console.log(this.name + ' says woof');
};
A function object is created.
Callable, with [[Prototype]] pointing to Function.prototype. So functions inherit .call, .apply, .bind.
An object is created for Dog.prototype.
Plain object. [[Prototype]] points to Object.prototype. Has one property: constructor, pointing back to Dog.
The two are linked.
Dog.prototype will become [[Prototype]] of every new Dog() instance.

What new Dog('Rex') actually does

// new Dog('Rex') is roughly equivalent to:
const obj = Object.create(Dog.prototype);  // 1. new object
const result = Dog.call(obj, 'Rex');          // 2. run with this = obj
return (typeof result === 'object' && result !== null)
  ? result : obj;                              // 3. return obj

When you declare a class

class Dog {
  constructor(name) { this.name = name; }
  bark() { console.log(this.name + ' says woof'); }
}

The same three things happen. Dog is still a function. Dog.prototype still exists. bark is placed on Dog.prototype.

Side by side

Function form

  • Callable WITHOUT new (works badly)
  • Hoisted to top of scope
  • Methods are enumerable

Class form

  • MUST use new (TypeError otherwise)
  • NOT hoisted (temporal dead zone)
  • Methods are non-enumerable
03 · The Anatomy

The full chain of an instance

Once you create an instance, three objects exist in a triangle. The relationships between them are the source of nearly every prototype quiz question.

Dog (the constructor function) name: 'Dog' prototype: ───→ __proto__: ─→ Function.prototype Dog.prototype (the shared methods object) bark: function constructor: ←── __proto__: ──→ Object.prototype toString hasOwnProperty __proto__: null rex (instance) name: 'Rex' __proto__: ──→ .constructor .prototype [[Prototype]] rex.[[Prototype]] === Dog.prototype

Fig. 2 — The triangle. Dog.prototype.constructor === Dog. The function and its prototype reference each other.

Why methods live on the prototype

const dogs = [];
for (let i = 0; i < 10000; i++) {
  dogs.push(new Dog('dog' + i));
}
// All 10,000 dogs share the SAME bark function:
dogs[0].bark === dogs[9999].bark  // true

If 10,000 dogs each carried their own copy of bark, that's 10,000 function objects in memory. With prototypes, there's one bark, shared.

04 · Inheritance

Extending the chain

Inheritance is just adding another link to the prototype chain. extends automates what used to take three lines of manual prototype manipulation.

class Animal {
  constructor(name) { this.name = name; }
  eat() { console.log(this.name + ' eats'); }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  bark() { console.log('woof'); }
}
rex name, breed Dog.prototype bark() Animal.prototype eat() Object.prototype toString() → null

Fig. 3 — Four objects, three links. Lookup walks left to right.

05 · The Loopholes

Where prototypes get weird

This is the section that costs people interviews. Each of these has fooled experienced engineers.

1. Shared mutable prototype properties

class Dog {
  toys = [];   // instance field — OK
}
Dog.prototype.friends = [];  // ⚠ shared by ALL dogs

const rex = new Dog();
const max = new Dog();
rex.friends.push('spot');
console.log(max.friends);  // ['spot'] — wat

Mutating friends through one instance mutates it for all. Instance fields are the safe pattern for mutable data.

2. Object.create(null) and the missing methods

const bare = Object.create(null);
bare.toString();    // TypeError

Objects with no prototype chain. Useful for dictionaries to avoid prototype pollution.

3. Arrow functions break this in prototypes

class Dog {
  name = 'Rex';
  bark = () => console.log(this.name);  // instance method
  woof() { console.log(this.name); }    // prototype method
}

bark is per-instance with this bound. woof is on prototype but loses this when passed as callback.

4. instanceof walks the prototype chain — it can lie

arr instanceof Array         // might be false across iframes!
Array.isArray(arr)             // ✓ the safe way

Mutating the chain or crossing realms breaks instanceof. This is why React leans on duck-typing.

5. for...in walks the entire chain — prototype pollution

const obj = { a: 1 };
Object.prototype.bad = 'gotcha';
for (const k in obj) console.log(k);  // 'a', then 'bad'

This is a real security CVE class. Lodash and jQuery have shipped fixes for it. Use Object.keys, Object.hasOwn(obj, k), or Map.

6. Modifying built-in prototypes broke the web (Smoosh-gate)

TC39 wanted to add Array.prototype.flatten until they discovered MooTools had patched it years prior. Standardizing broke production sites. The committee renamed it to flat.

7. Object.setPrototypeOf is a perf cliff

V8 optimizes objects by tracking their "shape" (hidden class). Changing prototype at runtime invalidates all those optimizations. Use Object.create at construction time.

The interview answer

"JavaScript uses prototypal inheritance, not classical. Every object has an internal [[Prototype]] slot pointing to another object. Property lookup walks that chain until it finds the property or hits null. Functions have a .prototype property, and new creates instances whose [[Prototype]] points to it. class is syntax over the same machinery."

The mental model

  1. Three terms, one slot. __proto__, [[Prototype]], Object.getPrototypeOf(). .prototype is unrelated.
  2. Class is sugar, not magic. typeof class Foo {} === 'function'.
  3. Methods on prototype, mutable state on instance.
  4. Mutating prototypes is dangerous. Prototype pollution is a CVE class.
  5. Prefer composition over deep inheritance hierarchies.
JS prototypes field guide · Frontend Field Guides

Before you leave — how confident are you with this?

Your honest rating shapes when you'll see this again. No grades, no shame.

Comments

to join the discussion.

Loading comments…

Keep reading