Prototype Chain: [[Prototype]], __proto__, and Object.create(null)
- Last updated
- Prerequisites:
- JavaScript objects and property lookup
- Basics of functions and constructors
- prototype
- inheritance
- prototype-chain
- object-create
- proto
Read at your depth
The practical view
Every object has an internal [[Prototype]] (the object it inherits from). Property lookup walks up this chain: obj.method looks on obj, then obj's prototype, then that one's, until found or reaching null. You read/set it with Object.getPrototypeOf / Object.setPrototypeOf, or the modern-ish __proto__ accessor. Object.create(proto) creates a fresh object with a chosen prototype, and Object.create(null) creates an object with NO prototype at all (no toString, no hasOwnProperty). Class syntax is sugar over this: class Dog extends Animal sets Dog.prototype's prototype to Animal.prototype. Property shadowing: defining a same-named property on the object stops the walk (the own property wins).
The same idea in other frameworks
React components use prototype delegation for class components (extends Component), while function components avoid the chain.Angular decorators and DI metadata leverage prototype-based inheritance on class hierarchies.Vue 3 dropped Vue 2's prototype-mutation reactivity; component options inheritance still uses class/object delegation.Svelte compiles components to plain JS objects and functions, rarely relying on user-facing prototype chains.Legacy vs modern
Object.create(null) vs a plain object literal for a dictionary
A null-prototype object avoids inherited Object.prototype keys that could collide with or hijack dictionary lookups.
const map = {}; // ❌ OLD: inherits from Object.prototype
map['hasOwnProperty'] = 'x'; // shadows an inherited method; lookups can collideconst map = Object.create(null); // ✅ NEW: no prototype chain at all
map['hasOwnProperty'] = 'x'; // safe: nothing inherited to collide withInterview gotchas
Context
Interviewers use this to distinguish developers who truly understand the prototype model from those who only remember syntax — the trio of __proto__, .prototype, and Object.create(null) is a reliable probe.
The mechanical answer
__proto__ is a legacy accessor on Object.prototype that reads/writes the [[Prototype]] of the object it is called on (settable via a setter). .prototype is a plain property that exists only on constructor functions (and classes) — it is the object that becomes the [[Prototype]] of instances created via new. Object.create(null) creates an object whose own [[Prototype]] is literally null, so its chain terminates immediately and it inherits no Object.prototype members. Mutating __proto__ on a normal object changes the live prototype used by the chain, which is why the prototype accessor is discouraged in favor of Object.setPrototypeOf or Object.create.
Trap
The classic trap is conflating __proto__ (on the instance, its prototype) with .prototype (on the constructor, used to build instances) — they point to related but different objects. Another trap: assuming Object.create(null) objects still have toString — they do not, because the chain is empty, so calling map.toString() throws unless you define it yourself.
Context
This connects the prototype model to a real vulnerability class — interviewers want you to show that the chain is live, shared, and mutable at runtime.
The mechanical answer
Property lookup walks the live prototype chain. If code merges untrusted JSON into an object using a naive deep merge that assigns __proto__, constructor.prototype, or prototype keys, it can inject properties onto a shared prototype (e.g. Object.prototype). Once a property like isAdmin is added to a shared prototype, every object that delegates to it reports that property as 'present' during lookup, even though no own property exists — enabling privilege escalation. Object.create(null) dictionaries are immune because they have no chain to pollute. Mitigations include never deep-merging untrusted keys, freezing prototypes, and using null-prototype maps for attacker-controlled keys.
Trap
The naive answer is 'pollution only affects the object being written'. Because the prototype is shared and lookup is dynamic, writing to a shared prototype affects every delegating object. Another trap: thinking JSON.parse output is safe — JSON.parse won't create setters, but a vulnerable merge that assigns __proto__ afterwards can still write the prototype slot on the target. Candidates who explain the live shared chain and the null-prototype fix demonstrate real security awareness.