← back to the section

JavaScript has no classical inheritance like Java or C# — it has prototypes. The class syntax only appeared in 2015 and under the hood desugars into the same prototypal mechanism. As long as you write simple classes, you can get by without knowing this; the moment you see hasOwnProperty, a method "out of nowhere", or this oddities — you can't make sense of it without prototypes.

The chain: where properties are looked up

Every object has a hidden reference to another object — its prototype. Property reads walk the chain: if the object itself doesn't have it — look in the prototype, then in the prototype's prototype, and so on until null.

const animal = { eats: true };
const rabbit = Object.create(animal); // rabbit's prototype is animal
rabbit.jumps = true;

rabbit.jumps; // true  — own property
rabbit.eats;  // true  — found IN THE PROTOTYPE
rabbit.toString(); // works — found in Object.prototype, the end of the chain

The key word is delegation: rabbit did not copy eats, it delegates the lookup upward. Change animal.eats — and what rabbit sees changes too. The object still has just one property (jumps), which the tooling shows:

Object.keys(rabbit);                 // ["jumps"] — own only
"eats" in rabbit;                    // true — in checks the whole chain
Object.hasOwn(rabbit, "eats");       // false — this one is own only

Hence the practice: when iterating over objects you don't control, distinguish "own" from "inherited" — Object.keys/Object.entries return only own properties, while the in operator and the for..in loop look into the chain.

Writing works differently from reading: the assignment rabbit.eats = false does not change animal — it creates an own eats property on rabbit that shadows the prototype one. The chain is read-only.

prototype and proto: two different words

The language's biggest terminology confusion:

  • __proto__ (officially — [[Prototype]], read via Object.getPrototypeOf(obj)) — that very hidden reference of any object to its prototype.
  • prototype — an ordinary property that exists only on functions: the object that will become the prototype of instances created via new.
function User(name) { this.name = name; }
User.prototype.greet = function () { return `Hello, ${this.name}`; };

const u = new User("Anna");
Object.getPrototypeOf(u) === User.prototype; // true — here is the link
u.greet(); // "Hello, Anna" — the method was found in the prototype

new does four things: creates an empty object, sets its prototype to Fn.prototype, calls the function with this = the new object, and returns it. Methods thus exist as a single copy shared by all — in the prototype, not copied into each object. That saving is the whole point of the machinery.

class is prototypes in nice clothes

class User {
  constructor(name) { this.name = name; }
  greet() { return `Hello, ${this.name}`; }
}

This code does the same thing as the function example above: greet lives in User.prototype, instances delegate to it. extends builds a chain of two prototypes. Easy to verify:

typeof User;                        // "function" — a class is a function
Object.hasOwn(new User("x"), "greet"); // false — the method is not on the instance

What class genuinely adds: new becomes mandatory (calling without it is an error), strict mode inside, private #field fields, tidy super syntax. What it does not add: no "real classical" inheritance — under the hood it's all the same delegation. Practical consequence: a class method passed as a callback loses this exactly like any function — the class doesn't save you from that.

Where this bites in practice

  • A method "out of nowhere": [].at, "".padStart — all from prototypes (Array.prototype, String.prototype). This is also why you must not patch built-in prototypes: by adding your own thing to Array.prototype, you change the behavior of every array in the application and in third-party libraries.
  • for..in over an array iterates both inherited properties and indexes as strings — for arrays use only for..of or array methods.
  • JSON and prototypes: JSON.parse creates objects with the regular prototype, whereas Object.create(null) gives an object with no prototype at all — a "pure dictionary" without toString, ideal for key-value maps where keys come from outside.
  • Type checks: instanceof walks the prototype chain, so it "breaks" if the object came from another window/iframe — those have their own built-in prototypes.

In short

  • Property reads walk the prototype chain down to null; writes always create an own property. This is delegation, not copying.
  • __proto__/[[Prototype]] — any object's reference to its prototype; prototype — a function's property, from which new takes the prototype for instances.
  • Methods live in the prototype as a single copy shared by all — which is why Object.keys doesn't see them but in does.
  • class is syntax on top of the same mechanism: methods in prototype, extends — a chain. The lost this of a callback method doesn't go away.
  • Don't patch built-in prototypes; for "pure dictionaries" — Object.create(null) or Map.
  • this: call context — the second half of the "methods and objects" puzzle.
  • Objects: references, copying, and immutability — the next topic of the series.
  • Closures — an alternative to prototypes for sharing behavior and state.