Skip to main content

Property lookup in the prototype chain

The prototype chain is the sequence of objects that the JavaScript engine walks upwards while looking for a property that the object itself does not have. The lookup starts with own properties, continues with the prototype, then the prototype's prototype, and ends at null.

Theory

TL;DR

  • Reading obj.prop: own properties of obj first, then obj.__proto__, then higher up, until null.
  • Found nowhere: the result is undefined, no error is thrown.
  • The lookup is dynamic: properties are not copied, the engine walks the chain every time (with caching).
  • Writing obj.prop = value always creates an own property on obj, the prototype stays unchanged.
  • Getters and setters found in the chain are executed, not returned as values.
  • Object.create(null) gives an object with an empty chain: the lookup stops immediately.

Quick example

javascript
const animal = { eats: true }; const dog = Object.create(animal); dog.barks = true; console.log(dog.barks); // 1) found on dog -> true console.log(dog.eats); // 2) not on dog -> look at animal -> true console.log(dog.toString); // 3) not on animal -> Object.prototype -> [Function] console.log(dog.fly); // 4) nowhere -> undefined

The chain here is:

javascript
dog -> animal -> Object.prototype -> null

Visually it is a stack of objects where each next one is the "fallback" for the previous:

javascript
dog { barks: true } ^ | animal { eats: true } ^ | Object.prototype { toString(), hasOwnProperty(), ... } ^ | null

The algorithm under the hood, step by step

When obj.prop runs, the engine does the following:

  1. Checks whether prop is an own property of obj. If so, it returns the value and stops.

  2. If not, it takes the object's prototype:

    javascript
    const parent = Object.getPrototypeOf(obj);
  3. Checks whether prop exists on parent.

  4. Repeats steps 2 and 3 until either:

    • the property is found, or
    • the prototype equals null.

If the loop reaches null, undefined is returned. Importantly, all of this happens dynamically, without copying properties: if you add a property to the prototype later, every descendant sees it right away.

Writing a property works differently

For obj.prop = value the mechanism is different:

  • If the property already exists on the object itself, it is simply overwritten.
  • If not, a new own property is created on obj, not on the prototype.
  • The prototype is not modified at all.
javascript
const animal = { eats: true }; const dog = Object.create(animal); dog.eats = false; // creates a new own property on dog console.log(dog.eats); // false, own property console.log(animal.eats); // true, on the prototype, unchanged

This overriding is called shadowing: an own property shadows the prototype property of the same name.

Reading and writing take different paths

OperationWhere it looksWhat it does
obj.prop (read)on obj, then up the chainreturns the first value found
obj.prop = value (write)writes to obj when writable: truedoes not change the prototype
delete obj.propon obj onlydoes not affect the prototype

That is exactly why delete dog.eats after shadowing restores access to animal.eats: the own property is removed while the prototype one stays in place.

Getters and setters in the chain

If a getter (get prop()) is found at any level of the chain, it is executed rather than returned as a function. Likewise, a setter that is found runs on assignment, and in that case no new own property is created on the object.

javascript
const animal = { get info() { return 'Animal info'; } }; const dog = Object.create(animal); console.log(dog.info); // "Animal info", the prototype's getter was called

So prototypes can hold not only data but computed, "reactive" properties too.

Deep chains and engine optimisation

The chain has no length limit, the lookup simply walks more levels:

javascript
const a = { level: 'A' }; const b = Object.create(a); const c = Object.create(b); const d = Object.create(c); console.log(d.level); // "A"

Here the engine inspects four objects before it finds level on a. In practice this is cheaper than it sounds, because JS engines (V8, SpiderMonkey and others) aggressively optimise prototype lookup:

  • they build internal "shape" maps, that is, descriptions of object structure;
  • they cache where exactly a property was found (inline caches);
  • they drop the cache as soon as the prototype or the set of properties changes.

That is why changing a prototype at runtime with Object.setPrototypeOf is a slow operation: it invalidates caches and forces the engine to deoptimise the code. Set the prototype at creation time instead: Object.create(proto) or class ... extends.

An object with no prototype

javascript
const pure = Object.create(null); pure.key = 'value'; console.log(pure.toString); // undefined, there is no Object.prototype

The chain ends immediately:

javascript
pure -> null

The lookup stops instantly. This is why Object.create(null) is handy for dictionaries: no key such as toString or constructor arrives from a prototype and spoils an if (dict[key]) check.

Summary

StageWhat happens
1The engine looks for the property on the object itself
2If not found, it goes to [[Prototype]]
3It keeps climbing the chain
4Reached null - returns undefined
WriteCreates an own property on the object, not on the prototype
AccessorsGetters and setters in the chain are taken into account and executed

Common mistakes

  • Thinking properties are copied into the object. Nothing is copied: dog does not contain eats, it merely finds it on animal on every read.
  • Expecting dog.eats = false to change the prototype. A write always creates an own property, animal.eats stays true.
  • Confusing in with hasOwnProperty. 'eats' in dog is true (it searches the whole chain), while Object.hasOwn(dog, 'eats') is false, because it checks own properties only.
  • Iterating with for...in without a filter. That loop also visits inherited enumerable properties; use Object.keys or Object.hasOwn.
  • Changing the prototype on the fly with Object.setPrototypeOf. It is correct but slow: the engine drops its caches. Set the prototype at creation time.
  • Extending built-in prototypes. Adding methods to Object.prototype or Array.prototype puts them in the chain of absolutely every object and breaks other people's code.
  • Forgetting about Object.create(null). Such an object has no toString, hasOwnProperty and similar methods, so pure.hasOwnProperty('key') throws.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.