JavaScript · Objects & Prototypes

JavaScript Prototypes & the Prototype Chain Explained — A Visual Guide

7 min read Updated 2026-06-18

Practice Prototypes & the Prototype Chain interview questions

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:

  1. Does obj have an own property foo? If yes, return it.
  2. If not, go to obj's prototype and check there.
  3. Repeat up the chain.
  4. If you reach the end (null) without finding it, the result is undefined.
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.
  • prototype is a property that exists only on functions. It's the object that will become the __proto__ of instances created with new.
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 via Object.getPrototypeOf) is the actual link on every object; prototype is 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 this varies.
  • instanceof is 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.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.