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:
| Term | Lives on | Is |
|---|---|---|
[[Prototype]] | Every object | The internal link to the object’s “parent.” |
prototype | A function | A 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:
- Does
objhave an own propertyx? Use it. - Otherwise, set
objtoObject.getPrototypeOf(obj)and repeat. - If
objbecomesnull, the result isundefined.
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.
Warning
Set the prototype at creation time (
Object.create(proto)or viaclass extends). AvoidObject.setPrototypeOfon objects that have already been used.
Why this matters even when you “just use classes”
class Foo extends Bar {}is exactlyFoo.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, orSymbol.toPrimitiveon 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.prototypeis a property on the functionFoo— the object that becomes the[[Prototype]]of everynew Foo().Object.create(proto)is the modern primitive for chain construction.Object.create(null)gives you a clean dictionary.instanceofsearches the chain forFoo.prototype;Array.isArrayis 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.










