The idea behind prototypes
JavaScript doesn't have classes in the way C++ or Java do — even the class keyword is
syntactic sugar over a much simpler, more flexible mechanism: prototypes. Every object
has a hidden internal link to another object, called its prototype. When you try to
read a property that an object doesn't have, the engine doesn't give up — it follows that
link to the prototype and looks there, then to that object's prototype, and so on. This
series of links is the prototype chain, and it is the single mechanism behind
inheritance, shared methods, and a lot of "magic" in the language.
Once you internalize property lookup as "walk up the chain until found or you hit null,"
huge swathes of JavaScript stop being mysterious.
Property lookup walks the chain
When you access obj.foo, the engine performs these steps:
- Does
objhave an own propertyfoo? If yes, return it. - If not, go to
obj's prototype and check there. - Repeat up the chain.
- If you reach the end (
null) without finding it, the result isundefined.
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 on animal via the chain
rabbit.flies // undefined — not found anywhere
rabbit itself has only jumps, but reading rabbit.eats succeeds because the lookup
climbs to animal. This is delegation: objects delegate to their prototypes for anything
they don't have themselves.
__proto__ vs prototype — the confusing pair
These two names trip up nearly everyone, because they sound the same but mean different things.
__proto__(or properly,Object.getPrototypeOf(obj)) is the actual prototype link that every object has — it points to the object it delegates to.prototypeis a property that exists only on functions. It's the object that will become the__proto__of instances created withnew.
function Dog(name) { this.name = name }
Dog.prototype.bark = function () { return 'woof' }
const d = new Dog('Rex')
Object.getPrototypeOf(d) === Dog.prototype // true
d.bark() // 'woof' — found on Dog.prototype
So Dog.prototype is a setup object (where you put shared methods), and each instance's
__proto__ points back to it. The function's own __proto__, by the way, is
Function.prototype — functions are objects too.
Reading the chain to the top
Follow any normal object's chain upward and you eventually reach Object.prototype, then
null.
const arr = [1, 2, 3]
Object.getPrototypeOf(arr) === Array.prototype // true
Object.getPrototypeOf(Array.prototype) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) // null <- the top
This is why every array has methods like map (from Array.prototype) and toString
(from Object.prototype): they live on prototypes the array delegates to. The chain for a
plain array literal is: arr -> Array.prototype -> Object.prototype -> null.
Creating objects with a specific prototype
Object.create(proto) is the most explicit way to set up a prototype link. It builds a new
object whose __proto__ is exactly the argument you pass.
const base = {
describe() { return `I am ${this.name}` }
}
const item = Object.create(base)
item.name = 'Widget'
item.describe() // 'I am Widget' — method borrowed from base
You can also create an object with no prototype by passing null, which is useful for
dictionaries where inherited keys would be a hazard:
const map = Object.create(null)
map.toString // undefined — no Object.prototype in its chain
'toString' in map // false truly empty
Shadowing: own properties win
If an object has its own property with the same name as one further up the chain, the own property "shadows" the inherited one — lookup stops at the first match.
const proto = { greet() { return 'hi from proto' } }
const obj = Object.create(proto)
obj.greet = () => 'hi from obj'
obj.greet() // 'hi from obj' own property shadows the prototype's
Importantly, assigning to obj.greet creates an own property on obj — it never
mutates the prototype. Writes always happen on the object itself (with one exception:
inherited setters run). This write-local, read-up-the-chain asymmetry is key to how
inheritance stays safe.
Why shared methods live on the prototype
Putting methods on the prototype rather than on each instance is a deliberate memory
optimization. If every Dog carried its own copy of bark, a thousand dogs would mean a
thousand identical functions. With the method on Dog.prototype, all instances share one
function, and this resolves to whichever instance called it.
function Dog(name) { this.name = name } // per-instance data
Dog.prototype.bark = function () { // shared behavior
return `${this.name} says woof`
}
const a = new Dog('A'), b = new Dog('B')
a.bark === b.bark // true same function object, different `this`
Rule of thumb: per-instance state goes on this; shared behavior goes on the prototype.
Inspecting and changing the chain
A few tools let you inspect and (rarely) modify prototypes:
Object.getPrototypeOf(obj) // read the prototype preferred
Object.setPrototypeOf(obj, newProto) // change it slow, avoid in hot paths
obj.isPrototypeOf?.(other) // is obj in other's chain?
child instanceof Parent // is Parent.prototype in child's chain?
instanceof is really a prototype-chain check in disguise: x instanceof C asks "is
C.prototype somewhere in x's chain?" That's why it works across inheritance levels.
Avoid Object.setPrototypeOf and __proto__ assignment at runtime. Changing an
object's prototype after creation forces engines to throw away optimizations, making
subsequent access dramatically slower. Set the prototype at creation time with
Object.create or new instead.
hasOwnProperty vs the chain
Because lookups climb the chain, you often need to distinguish own properties from inherited
ones — especially in for...in loops, which iterate inherited enumerable keys too.
const proto = { inherited: 1 }
const obj = Object.create(proto)
obj.own = 2
for (const key in obj) {
console.log(key, Object.hasOwn(obj, key))
// 'own' true
// 'inherited' false <- came from the prototype
}
Object.hasOwn(obj, key) (the modern, safe form of hasOwnProperty) returns true only
for own properties. Use it to filter for...in, or just prefer Object.keys, which never
includes inherited keys.
Performance and depth
Lookups are fast, but a very deep chain means more hops for a missed property. In practice, chains are shallow (two or three levels), so this rarely matters. What does matter is keeping the chain stable — mutating prototypes at runtime, as noted, is the real performance trap. Engines optimize objects with predictable shapes and chains; surprise them and you pay for it.
Key takeaways
- Every object has a hidden prototype link; missing-property lookups walk this chain
until found or
null. __proto__(read viaObject.getPrototypeOf) is the actual link on every object;prototypeis a property on functions that becomes instances' link.Object.create(proto)sets the chain explicitly;Object.create(null)makes a prototype-less object.- Own properties shadow inherited ones, and writes always land on the object itself.
- Shared methods go on the prototype so all instances reuse one function while
thisvaries. instanceofis a prototype-chain test; never reassign prototypes at runtime in hot code.
The prototype chain is the foundation everything else — constructors, class, inheritance —
is built on. Get this model crisp and the rest of the object system follows naturally.