JavaScript Prototypes and the Prototype Chain

A deep, practical explanation of JavaScript prototypes — what [[Prototype]] really is, how the prototype chain is searched, the difference between `__proto__` and `prototype`, Object.create, and how prototypes power inheritance under the surface of every class.

Why prototypes are still essential knowledge

ES6 added the class keyword, and many developers stopped thinking about prototypes. That is a mistake. Every JavaScript class is syntactic sugar over prototypes — and the moment you debug a bug involving instanceof, Object.create, or a “this method exists, why is it undefined?” question, you need the underlying model.

This post explains exactly how prototypes work. The next post shows the class syntax sitting on top.

Foundations: Objects — the basics.

Every object has a [[Prototype]]

Every JavaScript object carries an internal slot called [[Prototype]] — a reference to another object. When you read a property and the object does not have it, the engine follows that link and looks at the linked object. If still not found, it follows that object’s link, and so on. The terminus of every chain is null.

1const animal = { eats: true };
2const rabbit = { jumps: true };
3
4Object.setPrototypeOf(rabbit, animal);
5
6rabbit.jumps;   // true   ← own property
7rabbit.eats;    // true   ← from animal
8rabbit.flies;   // undefined ← end of chain

You can inspect the prototype with Object.getPrototypeOf(rabbit) — the modern, fast, correct way. The historical accessor __proto__ does the same thing but is deprecated for general use.

prototype vs [[Prototype]] — the source of all confusion

Two distinct things share an almost-identical name:

TermLives onIs
[[Prototype]]Every objectThe internal link to the object’s “parent.”
prototypeA functionA regular property; the object that becomes [[Prototype]] of any instance created with new.
1function Dog(name) { this.name = name; }
2Dog.prototype.bark = function () { return `${this.name} woofs`; };
3
4const rex = new Dog("Rex");
5
6Object.getPrototypeOf(rex) === Dog.prototype;   // true
7rex.bark();                                     // "Rex woofs"

So Dog.prototype is the object whose properties every new Dog() instance will inherit. The chain looks like:

rex  →  Dog.prototype  →  Object.prototype  →  null

How property lookup really works

When you write obj.x, the engine performs the following loop:

  1. Does obj have an own property x? Use it.
  2. Otherwise, set obj to Object.getPrototypeOf(obj) and repeat.
  3. If obj becomes null, the result is undefined.

The same loop runs for obj.method() — only the lookup is on the prototype chain; the call is on the original obj, so this inside the method refers to obj.

1const animal = { speak() { return `${this.name} speaks`; } };
2const rabbit = { name: "Bun" };
3Object.setPrototypeOf(rabbit, animal);
4
5rabbit.speak();   // "Bun speaks"  ← method found on animal, this is rabbit

This single rule explains why prototype-based inheritance feels different from class-based inheritance in other languages: methods are looked up each time they are called, so monkey-patching a prototype affects every existing instance.

Writing vs reading

The lookup loop only runs for reads. Writes always create an own property on the target object (unless they would hit a setter on the chain):

1const animal = { eats: true };
2const rabbit = Object.create(animal);
3
4rabbit.eats = false;          // creates own property `eats` on rabbit
5animal.eats;                  // still true

This is called property shadowing. It is how subclasses override methods without affecting the superclass.

Object.create — the modern building block

Object.create(proto, descriptors?) creates a new object whose [[Prototype]] is proto:

1const animal = { eats: true, speak() { return "..."; } };
2const rabbit = Object.create(animal);
3rabbit.jumps = true;

Object.create(null) creates an object with no prototype — a true “dictionary” that has no inherited toString, hasOwnProperty, etc. Useful when you are storing user-controlled keys and want to avoid collisions with built-ins.

1const dict = Object.create(null);
2dict.hasOwnProperty = "this is fine now";   // no clash

The chain of every literal

1({}).__proto__               === Object.prototype;        // true
2[].__proto__                 === Array.prototype;         // true
3Array.prototype.__proto__    === Object.prototype;        // true
4Object.prototype.__proto__   === null;                    // true
5
6(function(){}).__proto__     === Function.prototype;      // true

That is the entire built-in hierarchy: every array is also an object; every function is also an object; the buck stops at Object.prototype, whose [[Prototype]] is null.

instanceof — what it actually checks

x instanceof Foo returns true if Foo.prototype appears anywhere on x’s prototype chain:

1class Animal {}
2class Dog extends Animal {}
3
4const rex = new Dog();
5rex instanceof Dog;     // true
6rex instanceof Animal;  // true
7rex instanceof Object;  // true

This is also why instanceof can lie across realms (iframes, worker boundaries): each realm has its own Array.prototype, and an array from another realm fails instanceof Array even though it is functionally identical. Prefer Array.isArray for that case.

A mental map of the prototype model

Prototype performance

JavaScript engines optimise hot prototype chains aggressively. The price is paid when you mutate them: changing Object.setPrototypeOf on a live object invalidates a lot of inline caches and can slow surrounding code by orders of magnitude.

Why this matters even when you “just use classes”

  • class Foo extends Bar {} is exactly Foo.prototype = Object.create(Bar.prototype) plus some plumbing.
  • Adding a method to a class affects every existing instance — because instances do not own the method, they look it up on the prototype each call.
  • Overriding toString, Symbol.iterator, or Symbol.toPrimitive on a prototype changes how every instance is coerced.

Summary

  • Every object has an internal [[Prototype]] link. Property reads walk the chain; writes create own properties.
  • Foo.prototype is a property on the function Foo — the object that becomes the [[Prototype]] of every new Foo().
  • Object.create(proto) is the modern primitive for chain construction. Object.create(null) gives you a clean dictionary.
  • instanceof searches the chain for Foo.prototype; Array.isArray is the cross-realm safe check for arrays.
  • Mutating prototypes after the fact wrecks engine optimisations — set them once, at creation.

Next: ES6 classes — the syntax that sits on top of everything you just learned.

Attempted Questions: 0 / 8