Prototype methods, objects without __proto__

Earlier in this chapter you set a prototype with obj.__proto__. It works, but it is the old way in. This lesson covers the tools that replaced it, why they exist, and one specific place where __proto__ will quietly ruin your day if you keep using objects as dictionaries.

Reading or writing a prototype through obj.__proto__ is treated as legacy. The specification moved it into “Annex B”, the section reserved for features that only exist because browsers shipped them and the web depends on them. Non-browser JavaScript engines are not required to support it.

The modern accessors

Two functions do the reading and writing that __proto__ used to do:

  • Object.getPrototypeOf(obj) returns the [[Prototype]] of obj.
  • Object.setPrototypeOf(obj, proto) sets the [[Prototype]] of obj to proto.

There is one use of __proto__ that nobody frowns on: as a key inside an object literal when you first create the object, { __proto__: ... }. That form is fully standard now.

For creating an object with a chosen prototype there is also a dedicated method:

  • Object.create(proto[, descriptors]) builds a fresh empty object whose [[Prototype]] is proto, with an optional second argument of property descriptors.

Here is all of it at once:

let vehicle = {
  rolls: true
};

// create a new object with vehicle as its prototype
let scooter = Object.create(vehicle); // same as { __proto__: vehicle }

alert(scooter.rolls); // true

alert(Object.getPrototypeOf(scooter) === vehicle); // true

Object.setPrototypeOf(scooter, {}); // change scooter's prototype to {}
Object.create(vehicle){ proto: vehicle }→ [[Prototype]] = vehicle
Object.setPrototypeOf(obj, animal)changes an existing object’s link (slow)
Four ways to set a prototype at creation time — the top three are equivalent for a plain object.

Watch all three accessors work together below. scooter starts with vehicle as its prototype, so it inherits rolls. Read the prototype, then swap it out with Object.setPrototypeOf and see the inherited property disappear.

interactiveget / set / create a prototype

Descriptors as the second argument

Object.create can do more than { __proto__ }. Its second argument takes property descriptors, so you can define own properties on the new object in the same call:

let vehicle = {
  rolls: true
};

let scooter = Object.create(vehicle, {
  folds: {
    value: true
  }
});

alert(scooter.folds); // true

Those descriptors use the exact format from Property flags and descriptors. That means folds above is defined with the descriptor defaults: writable, enumerable, and configurable are all false unless you spell them out. So this form gives you finer control than a plain literal, at the cost of being wordier.

A genuinely exact clone

Because Object.create accepts both a prototype and a full set of descriptors, you can pair it with Object.getOwnPropertyDescriptors to copy an object faithfully:

let clone = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

This is stronger than copying keys with a for..in loop or Object.assign. It carries over everything: enumerable and non-enumerable properties, plain data properties, and getters/setters, all with their original flags. And it sets the same [[Prototype]] on the copy. A for..in copy, by contrast, would only reach enumerable inherited-and-own keys and would flatten any getter into a snapshot of its returned value.

This demo makes the difference concrete. The source object has a live getter now that reports the current time. Clone it both ways, then click “Read again” a second later: the Object.assign copy is frozen at the instant it was made, while the descriptor clone re-runs the getter and stays live.

interactiveassign flattens a getter; a descriptor clone keeps it live

Why so many ways to do one thing

prototype property, __proto__, Object.create, getPrototypeOf, setPrototypeOf — it is a lot of overlapping machinery for a single concept. The reason is history. Prototypal inheritance shipped with JavaScript in its earliest days, but the controls for it were added piece by piece over decades.

day oneF.prototype — oldest way to set a prototype via a constructor
2012Object.create — create with a prototype, but no get/set yet
(browsers)proto — non-standard accessor to get/set anytime
2015get/setPrototypeOf — standard replacement for proto
2022proto in {…} — literal form left Annex B; accessor form stayed
Timeline of prototype management in the language.

Walking through it:

  • The prototype property of a constructor function has worked since the beginning. It is the original way to make objects with a chosen prototype.
  • In 2012, Object.create landed. It let you create an object with a given prototype, but it did not let you read or change a prototype afterward. Around the same time, browsers shipped a non-standard __proto__ accessor to fill that gap and give developers on-the-fly access.
  • In 2015, Object.getPrototypeOf and Object.setPrototypeOf were standardized to do what __proto__ did. Since __proto__ was already implemented everywhere and the web relied on it, it was pushed into Annex B rather than removed.
  • In 2022, using __proto__ as a key in an object literal {...} was officially blessed and moved out of Annex B. The getter/setter form obj.__proto__ stayed in Annex B.

So there are two open questions. Why did functions replace __proto__? And why was the literal form rehabilitated while the accessor form stayed sidelined? The answer to both comes from a concrete flaw, which the second half of this lesson demonstrates.

“Very plain” objects

Objects double as associative arrays: bags of key/value pairs. That works beautifully until the keys come from outside your program — a user filling in a dictionary, say. Then one key misbehaves. Every string works as a key except "__proto__".

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object], not "some value"!

If the user types __proto__, the assignment on line 4 does nothing useful. Read it back and you get [object Object].

The reason follows from what __proto__ actually is. It is not a normal property slot — it is an accessor (a getter/setter pair) inherited from Object.prototype, and its setter insists that the value be an object or null. Hand it a string and the setter silently refuses. So obj["__proto__"] = "some value" is not stored anywhere; the setter runs, rejects the string, and the read afterward returns the current prototype instead.

Object.prototype
get proto() {…}
set proto(v) {…}
setter rejects non-objects
▲ [[Prototype]]
obj = {}
(no own proto key)
Reading or writing obj.__proto__ routes through an accessor up on Object.prototype — it never touches obj as a normal key.

In this toy example the fallout is mild. But swap strings for objects — imagine storing arbitrary JSON values under user keys — and an incoming __proto__ key will actually replace the object’s prototype. From there, property lookups behave in ways nobody planned for.

The dangerous part is that almost no one writes code expecting a key named __proto__. That makes these bugs easy to miss and, on a server, easy to turn into security holes (this class of issue is known as prototype pollution). The same trap applies to other inherited names like toString: assign to obj.toString and you may shadow a built-in that some other code relies on.

Option one: use a Map

The most direct fix is to stop using a plain object for arbitrary keys. A Map keeps its entries completely separate from any prototype, so no key is special:

let map = new Map();

let key = prompt("What's the key?", "__proto__");
map.set(key, "some value");

alert(map.get(key)); // "some value" (as intended)

That is the right tool much of the time. Still, object syntax is compact and familiar, and sometimes you want it.

Option two: an object with no prototype

You can keep object syntax and drop the trap by removing the prototype entirely. An object created with Object.create(null) has [[Prototype]] equal to null, so there is no inherited __proto__ accessor to intercept anything:

let obj = Object.create(null);
// or: obj = { __proto__: null }

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

With no prototype in the chain, __proto__ is just an ordinary key. The assignment stores a normal property and the read returns it.

Try it yourself. Type a key, store "some value" under it in both a plain {} and an Object.create(null) dictionary, then read it back. Ordinary keys behave the same in both. But type __proto__ and watch the plain object betray you while the null-prototype object stays honest.

interactiveThe __proto__ dictionary trap
let obj = {}
Object.prototype
has proto accessor
obj
let obj = Object.create(null)
null
no accessors, no methods
obj
A normal object inherits from Object.prototype; a null-prototype object has an empty chain.

These are sometimes called “very plain” or “pure dictionary” objects. They are even barer than a normal {...}, which is the point.

The trade-off: with no prototype, there are no inherited methods at all. toString is gone, so the object cannot be coerced to a string the usual way:

let obj = Object.create(null);

alert(obj); // Error (no toString)

For a dictionary you rarely care. And the important helpers live on Object itself, not on the prototype, so they keep working:

let colorHexes = Object.create(null);
colorHexes.teal = "#0f766e";
colorHexes.rose = "#e11d48";

alert(Object.keys(colorHexes)); // teal,rose

Object.keys, Object.values, Object.entries, Object.assign and friends all take the object as an argument, so a missing prototype does not affect them.

Summary

  • To create an object with a given prototype:

    • literal syntax { __proto__: ... }, which also lets you add other properties inline;
    • or Object.create(proto[, descriptors]), which lets you specify full property descriptors.

    Object.create gives you a faithful shallow copy in one expression:

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
  • The modern get/set methods are:

    • Object.getPrototypeOf(obj) — returns the [[Prototype]] (what the __proto__ getter did).
    • Object.setPrototypeOf(obj, proto) — sets the [[Prototype]] (what the __proto__ setter did).
  • Using the built-in obj.__proto__ getter/setter is discouraged; it now lives in Annex B of the specification.

  • Prototype-less objects, made with Object.create(null) or { __proto__: null }, work well as dictionaries for arbitrary (including user-supplied) keys. A normal object inherits built-in methods and the __proto__ accessor from Object.prototype, which occupy those key names and can cause side effects. A null-prototype object is truly empty, so no key is special.