Skip to content
JavaScript js prototypes 4 min read

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:

ApproachPurposeRecommended?
Object.getPrototypeOf(obj)Read the prototypeYes — the standard way
Object.setPrototypeOf(obj, proto)Change the prototypeUse rarely; hurts performance
obj.__proto__Legacy getter/setter for the prototypeAvoid 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 prefer Object.getPrototypeOf / Object.setPrototypeOf for clarity and to avoid edge cases (for instance, an object created with Object.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 ({}) get Object.prototype.
  • Arrays get Array.prototype (which itself links to Object.prototype).
  • new Fn() sets the new object’s prototype to Fn.prototype.
  • class does the same — methods land on Class.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.getPrototypeOf to inspect prototypes and Object.create to set one at creation time — both are explicit and standard.
  • Avoid obj.__proto__; reserve Object.setPrototypeOf for 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 = 1 touched the prototype.
  • Use hasOwnProperty (or Object.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).
Last updated June 1, 2026
Was this helpful?