skip to content

Why is `__proto__` considered legacy in JavaScript, and which standard APIs replace reading and writing it?

level: middleimportance: should knowfreq 58%

answer

  1. not core spec, kept for the web
  2. it lives on one shared object
  3. a getter/setter pair, not a slot
  4. absent when the chain does not include it
  5. Object.getPrototypeOf and setPrototypeOf instead

basics

~10 s

proto is a legacy accessor defined on Object.prototype and standardised only in the web-compatibility annex. Object.getPrototypeOf reads an object's prototype link and Object.setPrototypeOf writes it; both are core-language functions that work on any object.

solid answer

~40 s

`__proto__` predates standardisation: it was a browser extension the committee later wrote down in the web-compatibility annex as *normatively optional*, kept only because too much existing code depended on it. Mechanically it is an inherited accessor pair — a getter and setter defined on `Object.prototype` — not a real slot, so it works only for objects that actually inherit from `Object.prototype`. The core-spec replacements are `Object.getPrototypeOf(obj)` (ES5) and `Object.setPrototypeOf(obj, proto)` (ES2015), plain functions with no such dependency. There is also a data-safety angle: because `__proto__` is a magic *name*, code that copies untrusted keys with `target[key] = value` can hit the setter and change an object's prototype instead of storing data — the shape behind prototype-pollution bugs. Using the explicit functions makes the intent, and the failure mode, unambiguous.

code

javascript · 9 lines
javascript
const plain = {};
console.log(plain.__proto__ === Object.prototype);            // true
console.log(Object.getPrototypeOf(plain) === Object.prototype); // true

// the accessor is inherited, so an object outside that chain lacks it
const bag = Object.create(null);
console.log(bag.__proto__);            // undefined
bag.__proto__ = { x: 1 };              // plain own data property
console.log(Object.getPrototypeOf(bag)); // null — unchanged

go deeper

for a junior

Recall that proto is the old way and that Object.getPrototypeOf reads an object's prototype while Object.setPrototypeOf changes it; prefer the functions in code you write.

for a middle

Explain that proto is a getter/setter pair inherited from Object.prototype and specified only in the web-compatibility annex, and show what happens on an object that does not inherit it.

for a senior

Demonstrate the data-flow judgment: identify where an untrusted key can reach a plain assignment, explain why that is the prototype-pollution shape, and name the guard you would apply.

for a principal

Own the policy — a lint rule banning proto in source, a convention for how untrusted key/value data enters the system, and a decision on whether shared lookup tables use null-prototype objects or Map.

## Where `__proto__` came from `__proto__` was never designed as part of the core language. Browsers shipped it as a convenient way to expose the internal `[[Prototype]]` slot, code came to depend on it, and when the committee could no longer remove it they documented it in the annex of web-compatibility features — the part of the specification marked *normative optional*, meaning a conforming engine outside a web browser is allowed not to have it. Every major engine does implement it, but its status is "we are stuck with this", not "this is the way". The core-language replacements are ordinary functions: ```js Object.getPrototypeOf(obj); // ES5 — read the link Object.setPrototypeOf(obj, someProto); // ES2015 — write the link (returns obj) ``` ## It is an inherited accessor, not a slot This is the mechanical detail that matters. `__proto__` is a single accessor property — a getter/setter pair — defined on `Object.prototype`. When you write `obj.__proto__`, ordinary property lookup walks up to `Object.prototype`, finds the getter, and calls it with `obj` as the receiver; the getter returns that object's internal prototype link. Assignment calls the setter. Everything follows from that. If an object does **not** inherit from `Object.prototype`, the accessor is simply not in its chain, so: ```js const bag = Object.create(null); bag.__proto__; // undefined — no getter anywhere in the chain bag.__proto__ = { x: 1 }; // creates an ordinary own data property! Object.getPrototypeOf(bag); // still null — the prototype did not change ``` On such an object `__proto__` is just a string key like any other. `Object.getPrototypeOf` has no such hole: it reads the internal slot directly, whatever the object's chain looks like. ## The name is magic, and that is a hazard Because `__proto__` is a property *name* rather than syntax, any code that takes keys from data and assigns them can trip the setter by accident: ```js function merge(target, source) { for (const key of Object.keys(source)) target[key] = source[key]; return target; } merge({}, JSON.parse('{"__proto__": {"admin": true}}')); ``` The parse itself is safe: `JSON.parse` defines own properties directly rather than going through setters, so the result is a plain object with an own key literally named `__proto__`. The danger is the copy loop — `target[key] = value` is a normal assignment, which finds the inherited setter and changes `target`'s prototype instead of storing data. If the target is shared, or if the same pattern is applied to an object whose prototype is `Object.prototype` itself, effects can reach far beyond the one call. Guarding the key explicitly (skipping `__proto__`, or using `Object.defineProperty`, `Object.hasOwn`, or a `Map`) is the fix; using `Object.setPrototypeOf` where you *do* mean to change a prototype makes the intent visible. ## Reading versus writing The two directions differ in cost as well as in style. `Object.getPrototypeOf` is a cheap read. Writing — whether through `Object.setPrototypeOf` or the `__proto__` setter — mutates an object's shape after creation and is expensive in every major engine, so it is something to avoid in hot code regardless of which spelling you use. Writes also have real failure modes worth knowing: ```js const fixed = Object.preventExtensions({}); Object.setPrototypeOf(fixed, { a: 1 }); // TypeError — not extensible Object.setPrototypeOf({}, null); // fine — null is a valid prototype ``` ## What to write in review Use `Object.getPrototypeOf` and `Object.setPrototypeOf` in code you own. Treat a bare `obj.__proto__` in a diff as either legacy or a bug, and treat `'__proto__'` appearing as a *data* key as a signal to check every place that key can flow into an assignment. The one place `__proto__` still reads naturally is a quick console inspection, where its brevity is the whole point and nothing is being shipped.

  • Why does `obj.__proto__` return `undefined` for some objects while `Object.getPrototypeOf(obj)` still works?
    Because `__proto__` is an accessor defined on `Object.prototype`, reached by ordinary lookup. An object whose chain does not include `Object.prototype` never finds that getter, so the read yields `undefined` and an assignment creates a normal own property named `__proto__`. `Object.getPrototypeOf` is a standalone function that reads the internal slot directly and is unaffected by what the object inherits.
  • Does `JSON.parse` on input containing a `__proto__` key change the resulting object's prototype?
    No. `JSON.parse` creates own properties directly rather than performing assignments, so the result is a plain object carrying an own key literally named `__proto__`; its prototype is untouched. The risk appears afterwards, when code copies those keys with `target[key] = value` — that is a real assignment, it finds the inherited setter, and it mutates the target's prototype.
  • When is reaching for `Object.setPrototypeOf` in application code a smell?
    Almost always. Changing an existing object's prototype is expensive in every major engine and makes the object's shape unpredictable to readers. Objects should be created with the prototype they need; retrofitting one usually means the construction path is wrong. Legitimate uses are narrow — fixing up an object handed to you by code you do not control, or an interop shim — and they belong in one isolated place, not scattered through business logic.

saying these in an interview costs you the question

  • Says __proto__ is a normal core-spec property
  • Thinks __proto__ is a per-object internal slot
  • Believes JSON.parse itself pollutes the prototype
  • Assumes every object exposes a __proto__ accessor
  • Uses __proto__ and setPrototypeOf as interchangeable style choices

context