JAVASCRIPT / PROTOTYPES, CLASSES, AND INHERITANCE
The prototype chain and property lookup
Trace how JavaScript resolves properties along an object's prototype chain, tell own from inherited members, and predict when assignment shadows.
What you will learn
- Walk any chain with Object.getPrototypeOf until it returns null.
- Separate own from inherited state using Object.hasOwn and the in operator.
- Explain why an inherited method's this is the object the lookup started from.
- Predict that obj.x = v creates an own property instead of changing the prototype.
Understanding The prototype chain and property lookup
Every JavaScript object carries a hidden link, written [[Prototype]] in the specification, pointing either to another object or to null. When you read obj.x the engine checks obj's own properties first; if x is not there it follows the link, checks that object, and keeps going until it finds x or reaches null, in which case the result is undefined rather than an error. Object.getPrototypeOf(obj) reads that link from code, and following it repeatedly always terminates, because chains may not contain cycles and Object.prototype's link is null.
The consequence that matters is that lookup is a search performed on every single access, not a copy made when the object was created. Add a method to a prototype long after its descendants exist and they will find it immediately, because nothing was ever copied into them. When the match is several links up, this is still bound to the object the search started from, the receiver, which is why one shared function can read per-object state and why moving a method up or down the chain does not change which data it operates on.
Writing is asymmetric. obj.x = v consults the chain only to check for an inherited setter or a non-writable inherited property; in the ordinary case it creates or updates an own property on obj and leaves every prototype untouched. That own property then shadows the inherited one for that object alone, which is why Object.hasOwn(obj, 'x') and 'x' in obj answer different questions: the first asks whether the object holds x itself, the second asks whether a lookup would find x anywhere along the chain.
const base = { kind: 'base', describe() { return `kind=${this.kind}`; } };
const middle = Object.create(base);
middle.kind = 'middle';
const leaf = Object.create(middle);
console.log(leaf.describe());
console.log(Object.hasOwn(leaf, 'kind'), 'kind' in leaf);
leaf.kind = 'leaf';
console.log(leaf.describe(), middle.kind, base.kind);
const labels = new Map([
[middle, 'middle'],
[base, 'base'],
[Object.prototype, 'Object.prototype']
]);
const hops = [];
let link = Object.getPrototypeOf(leaf);
while (link !== null) {
hops.push(labels.get(link));
link = Object.getPrototypeOf(link);
}
console.log(hops.join(' -> ') + ' -> null');Property reads search the object and then each link of its prototype chain until a match or null, while ordinary writes stop at the object you started from.
Worked examples
The chain behind a plain array
Shows that array methods are never own properties and that every chain ends at null.
const nums = [10, 20];
console.log(Object.hasOwn(nums, 'map'), typeof nums.map);
console.log(Object.getPrototypeOf(nums) === Array.prototype);
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));
console.log(nums.length, Object.hasOwn(nums, 'length'));Example explained
Line 1map is not stored on the array; the lookup finds it one link up on Array.prototype, which is why thousands of arrays share one function object.
Line 2Array.prototype's own link points at Object.prototype, so arrays also resolve toString and hasOwnProperty two links away.
Line 3Object.getPrototypeOf(Object.prototype) is null: the end of the chain, and the reason a missing property evaluates to undefined instead of throwing.
Line 4length is own on each array because it describes that array alone and cannot be shared.
Shadowing and shared mutable state
Demonstrates that assignment writes to the receiver while mutation reaches the prototype's object.
const proto = { count: 0, tags: [] };
const a = Object.create(proto);
const b = Object.create(proto);
a.count++;
console.log(a.count, b.count, proto.count);
console.log(Object.hasOwn(a, 'count'), Object.hasOwn(b, 'count'));
a.tags.push('x');
console.log(b.tags.length, Object.hasOwn(a, 'tags'));
delete a.count;
console.log(a.count);Example explained
Line 1a.count++ reads 0 through the chain and then writes 1 as a new own property on a, so proto and b are unaffected.
Line 2Object.hasOwn confirms the split: a now holds its own count, b still borrows the prototype's.
Line 3a.tags.push mutates the single array the prototype holds, so b sees length 1 even though neither a nor b owns a tags property.
Line 4delete removes only a's own count, which unmasks the inherited 0 again.
A setter on the prototype intercepts the write
Shows the one common case where assignment does not create an own property of that name.
const store = {
_raw: 0,
get value() { return this._raw; },
set value(v) { this._raw = v * 2; }
};
const item = Object.create(store);
item.value = 5;
console.log(item.value, store.value);
console.log(Object.hasOwn(item, 'value'), Object.hasOwn(item, '_raw'));
console.log(store._raw);Example explained
Line 1item.value = 5 searches the chain, finds an accessor on store, and calls its setter instead of defining an own value property.
Line 2Inside the setter this is item, the receiver, so this._raw = 10 creates an own data property on item.
Line 3The getter then runs with this === item and returns 10, while store.value still reads store's own _raw of 0.
Line 4hasOwn reports false for value and true for _raw, which is exactly where the two writes landed.
Important notes
delete only removes own properties: deleting a shadow makes the inherited value visible again, and delete obj.x does nothing at all when x lives on a prototype.
Prefer Object.hasOwn(obj, key) over obj.hasOwnProperty(key), since hasOwnProperty is itself resolved through the chain and is missing on objects created with a null prototype.
Common mistakes
Reaching for obj.prototype to get the link. Only functions have a .prototype property, so obj.prototype is undefined and obj.prototype.foo = 1 throws TypeError: Cannot set properties of undefined; the link is Object.getPrototypeOf(obj).
Assuming proto.items.push('x') gives each object its own list. The read resolves to the prototype's single array, so every object delegating to that prototype sees the pushed item and the bug looks like state leaking between instances.
Using for...in to list an object's own keys. It walks the whole chain and yields enumerable inherited keys too, so custom prototype properties show up as if they were data on the object; use Object.keys or filter with Object.hasOwn.
Try it yourself
Change, predict, then run
In a browser console build const a = {}, b = Object.create(a), c = Object.create(b), then add a.hello = () => 'hi' after c already exists and confirm c.hello() still resolves. Now define your own c.hello, check Object.hasOwn(c, 'hello'), delete it, and show that a's version was never modified.
Open the JavaScript workspaceCheck your understanding
Given const proto = { count: 0 }; const a = Object.create(proto); const b = Object.create(proto); a.count++; what do a.count, b.count and proto.count report?
- 1, 0, 0 because the read came from proto but the write landed on a
- 1, 1, 1 because all three names refer to the same storage slot
- 1, 0, 1 because the increment wrote through the chain to proto
- NaN, 0, 0 because a.count is undefined before the increment runs
Show answer
a.count++ reads through the chain, finds 0 on proto, then assigns to a, and ordinary assignment always creates an own property on the receiver, so proto and b keep the original 0. The 1, 0, 1 option assumes writes travel up the chain; that only happens when the prototype exposes a setter for the name, and count here is a plain data property.