Prototypes Explained
JavaScript does not have classes at its core — even the class keyword is sugar over a much simpler idea: prototypes. Every object holds a hidden link to another object, called its prototype, and when you read a property that the object itself does not have, the engine quietly follows that link to look for it. Understanding this single mechanism explains inheritance, method sharing, and a whole class of “where did that property come from?” surprises.
What a prototype is
A prototype is just an ordinary object. Every JavaScript object carries an internal reference to one, specified by the language as [[Prototype]]. When you access obj.foo, the engine first checks obj itself. If foo is not an own property, it follows [[Prototype]] to the next object and checks there — and so on. This is delegation: an object delegates lookups it can’t satisfy to its prototype.
const animal = {
eat() {
return `${this.name} is eating`;
},
};
const dog = Object.create(animal);
dog.name = "Rex";
console.log(dog.eat());
console.log(dog.hasOwnProperty("eat"));
console.log(dog.hasOwnProperty("name"));
Output:
Rex is eating
false
true
dog has no eat method of its own. The lookup falls back to animal, where eat lives. Notice that this inside eat still refers to dog — the object the call started from — not to the prototype where the method was found.
Reading the prototype: [[Prototype]], __proto__, and getPrototypeOf
[[Prototype]] is an internal slot you cannot touch by name. The language exposes it three different ways, and they are not equal in quality:
| Approach | Purpose | Recommended? |
|---|---|---|
Object.getPrototypeOf(obj) | Read the prototype | Yes — the standard way |
Object.setPrototypeOf(obj, proto) | Change the prototype | Use rarely; hurts performance |
obj.__proto__ | Legacy getter/setter for the prototype | Avoid in new code |
const animal = { eat() {} };
const dog = Object.create(animal);
console.log(Object.getPrototypeOf(dog) === animal);
console.log(dog.__proto__ === animal);
Output:
true
true
__proto__is a deprecated accessor kept alive only for web compatibility. It works in every engine, but preferObject.getPrototypeOf/Object.setPrototypeOffor clarity and to avoid edge cases (for instance, an object created withObject.create(null)has no__proto__accessor at all).
How the lookup falls back
The fallback walks one prototype at a time until it finds the property or reaches the end of the chain, which is null. Plain object literals link to Object.prototype, which is why every object “has” toString and hasOwnProperty.
dog ───▶ animal ───▶ Object.prototype ───▶ null
│ │ │
name eat toString
sleep hasOwnProperty
When you ask for dog.toString, the engine checks dog (no), then animal (no), then Object.prototype (found). Reaching null without a match yields undefined — it never throws.
const animal = { eat() {} };
const dog = Object.create(animal);
console.log(Object.getPrototypeOf(animal) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));
console.log(dog.toString());
console.log(dog.somethingMissing);
Output:
true
null
[object Object]
undefined
Writes don’t delegate
Reading delegates up the chain, but assignment always creates or updates an own property on the target object. It does not modify the prototype. This is what keeps shared prototype methods safe from accidental mutation.
const animal = { legs: 4 };
const dog = Object.create(animal);
console.log(dog.legs); // read: delegated to animal
dog.legs = 3; // write: creates an OWN property on dog
console.log(dog.legs); // own value shadows the prototype
console.log(animal.legs); // animal is untouched
Output:
4
3
4
The own legs on dog now shadows the inherited one. Delete it and the prototype’s value becomes visible again.
Where prototypes come from
You rarely call Object.create by hand. Most prototypes are wired up for you:
- Object literals (
{}) getObject.prototype. - Arrays get
Array.prototype(which itself links toObject.prototype). new Fn()sets the new object’s prototype toFn.prototype.classdoes the same — methods land onClass.prototype, shared by every instance.
console.log(Object.getPrototypeOf([]) === Array.prototype);
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype);
class User {
greet() {
return "hi";
}
}
const u = new User();
console.log(Object.getPrototypeOf(u) === User.prototype);
console.log(u.hasOwnProperty("greet")); // method is on the prototype, not the instance
Output:
true
true
true
false
Best practices
- Use
Object.getPrototypeOfto inspect prototypes andObject.createto set one at creation time — both are explicit and standard. - Avoid
obj.__proto__; reserveObject.setPrototypeOffor the rare case where the chain truly must change after creation, since reassigning it deoptimizes objects in most engines. - Remember that reads delegate up the chain but writes create own properties — never assume
obj.x = 1touched the prototype. - Use
hasOwnProperty(orObject.hasOwn(obj, key)in modern runtimes) when you need to distinguish own properties from inherited ones. - Put shared methods on a prototype, not on each instance, so all instances share one function reference and stay memory-light.
- For a clean dictionary object with no inherited keys, create it with
Object.create(null).