15 Desember 2021

Prototype methods, objects without __proto__

In the first chapter of this section, we mentioned that there are modern methods to setup a prototype.

The __proto__ is considered outdated and somewhat deprecated (in browser-only part of the JavaScript standard).

The modern methods are:

These should be used instead of __proto__.

For instance:

let animal = {
  eats: true
};

// create a new object with animal as a prototype
let rabbit = Object.create(animal);

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}

Object.create has an optional second argument: property descriptors. We can provide additional properties to the new object there, like this:

let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

alert(rabbit.jumps); // true

The descriptors are in the same format as described in the chapter Properti flag dan Deskriptor.

We can use Object.create to perform an object cloning more powerful than copying properties in for..in:

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

This call makes a truly exact copy of obj, including all properties: enumerable and non-enumerable, data properties and setters/getters – everything, and with the right [[Prototype]].

Brief history

If we count all the ways to manage [[Prototype]], there are a lot! Many ways to do the same thing!

Why?

That’s for historical reasons.

  • The "prototype" property of a constructor function has worked since very ancient times.
  • Later, in the year 2012, Object.create appeared in the standard. It gave the ability to create objects with a given prototype, but did not provide the ability to get/set it. So browsers implemented the non-standard __proto__ accessor that allowed the user to get/set a prototype at any time.
  • Later, in the year 2015, Object.setPrototypeOf and Object.getPrototypeOf were added to the standard, to perform the same functionality as __proto__. As __proto__ was de-facto implemented everywhere, it was kind-of deprecated and made its way to the Annex B of the standard, that is: optional for non-browser environments.

As of now we have all these ways at our disposal.

Why was __proto__ replaced by the functions getPrototypeOf/setPrototypeOf? That’s an interesting question, requiring us to understand why __proto__ is bad. Read on to get the answer.

Don’t change [[Prototype]] on existing objects if speed matters

Technically, we can get/set [[Prototype]] at any time. But usually we only set it once at the object creation time and don’t modify it anymore: rabbit inherits from animal, and that is not going to change.

And JavaScript engines are highly optimized for this. Changing a prototype “on-the-fly” with Object.setPrototypeOf or obj.__proto__= is a very slow operation as it breaks internal optimizations for object property access operations. So avoid it unless you know what you’re doing, or JavaScript speed totally doesn’t matter for you.

"Very plain" objects

As we know, objects can be used as associative arrays to store key/value pairs.

…But if we try to store user-provided keys in it (for instance, a user-entered dictionary), we can see an interesting glitch: all keys work fine except "__proto__".

Check out the example:

let obj = {};

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

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

Here, if the user types in __proto__, the assignment is ignored!

That shouldn’t surprise us. The __proto__ property is special: it must be either an object or null. A string can not become a prototype.

But we didn’t intend to implement such behavior, right? We want to store key/value pairs, and the key named "__proto__" was not properly saved. So that’s a bug!

Here the consequences are not terrible. But in other cases we may be assigning object values, and then the prototype may indeed be changed. As a result, the execution will go wrong in totally unexpected ways.