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 ofobjfirst, thenobj.__proto__, then higher up, untilnull. - 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 = valuealways creates an own property onobj, 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
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 -> undefinedThe chain here is:
dog -> animal -> Object.prototype -> nullVisually it is a stack of objects where each next one is the "fallback" for the previous:
dog {
barks: true
}
^
|
animal {
eats: true
}
^
|
Object.prototype {
toString(), hasOwnProperty(), ...
}
^
|
nullThe algorithm under the hood, step by step
When obj.prop runs, the engine does the following:
-
Checks whether
propis an own property ofobj. If so, it returns the value and stops. -
If not, it takes the object's prototype:
javascriptconst parent = Object.getPrototypeOf(obj); -
Checks whether
propexists onparent. -
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.
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, unchangedThis overriding is called shadowing: an own property shadows the prototype property of the same name.
Reading and writing take different paths
| Operation | Where it looks | What it does |
|---|---|---|
obj.prop (read) | on obj, then up the chain | returns the first value found |
obj.prop = value (write) | writes to obj when writable: true | does not change the prototype |
delete obj.prop | on obj only | does 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.
const animal = {
get info() {
return 'Animal info';
}
};
const dog = Object.create(animal);
console.log(dog.info); // "Animal info", the prototype's getter was calledSo 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:
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
const pure = Object.create(null);
pure.key = 'value';
console.log(pure.toString); // undefined, there is no Object.prototypeThe chain ends immediately:
pure -> nullThe 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
| Stage | What happens |
|---|---|
| 1 | The engine looks for the property on the object itself |
| 2 | If not found, it goes to [[Prototype]] |
| 3 | It keeps climbing the chain |
| 4 | Reached null - returns undefined |
| Write | Creates an own property on the object, not on the prototype |
| Accessors | Getters and setters in the chain are taken into account and executed |
Common mistakes
- Thinking properties are copied into the object. Nothing is copied:
dogdoes not containeats, it merely finds it onanimalon every read. - Expecting
dog.eats = falseto change the prototype. A write always creates an own property,animal.eatsstaystrue. - Confusing
inwithhasOwnProperty.'eats' in dogistrue(it searches the whole chain), whileObject.hasOwn(dog, 'eats')isfalse, because it checks own properties only. - Iterating with
for...inwithout a filter. That loop also visits inherited enumerable properties; useObject.keysorObject.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.prototypeorArray.prototypeputs them in the chain of absolutely every object and breaks other people's code. - Forgetting about
Object.create(null). Such an object has notoString,hasOwnPropertyand similar methods, sopure.hasOwnProperty('key')throws.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.