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]]ofobj.Object.setPrototypeOf(obj, proto)sets the[[Prototype]]ofobjtoproto.
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]]isproto, 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 {}
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.
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.
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.
Walking through it:
- The
prototypeproperty of a constructor function has worked since the beginning. It is the original way to make objects with a chosen prototype. - In 2012,
Object.createlanded. 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.getPrototypeOfandObject.setPrototypeOfwere 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 formobj.__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.
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.
has proto accessor
no accessors, no methods
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.creategives you a faithful shallow copy in one expression:let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); - literal syntax
-
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 fromObject.prototype, which occupy those key names and can cause side effects. Anull-prototype object is truly empty, so no key is special.