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 viaObject.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 vianew.
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 toArray.prototype, you change the behavior of every array in the application and in third-party libraries. for..inover an array iterates both inherited properties and indexes as strings — for arrays use onlyfor..ofor array methods.- JSON and prototypes:
JSON.parsecreates objects with the regular prototype, whereasObject.create(null)gives an object with no prototype at all — a "pure dictionary" withouttoString, ideal for key-value maps where keys come from outside. - Type checks:
instanceofwalks 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 whichnewtakes the prototype for instances.- Methods live in the prototype as a single copy shared by all — which is why
Object.keysdoesn't see them butindoes. classis 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)orMap.
What to read next
- 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.