__proto__ in ECMAScript 6

The property __proto__ (pronounced “dunder proto”) has existed for a while in most JavaScript engines. This blog post explains how it worked prior to ECMAScript 6 and what changes with ECMAScript 6.




For this blog post, it helps if you know what prototype chains are. Consult Sect. “Layer 2: The Prototype Relationship Between Objects” in “Speaking JavaScript”, if necessary.




__proto__ prior to ECMAScript 6

Prototypes

Each object in JavaScript starts a chain of one or more objects, a so-called prototype chain. Each object points to its successor, its prototype via the internal property [[Prototype]] (which is null if there is no successor). That property is called internal, because it only exists in the language specification and cannot be directly accessed from JavaScript. In ECMAScript 5, the standard way of getting the prototype p of an object obj is:



var p = Object.getPrototypeOf(obj);

There is no standard way to change the prototype of an existing object, but you can create a new object obj that has the given prototype p:



var obj = Object.create(p);

__proto__

A long time ago, Firefox got the non-standard property __proto__. Other browsers eventually copied that feature, due to its popularity.


Prior to ECMAScript 6, __proto__ worked in obscure ways:



  • You could use it to get or set the prototype of any object:



    var obj = {};
    var p = {};

    console.log(obj.__proto__ === p); // false
    obj.__proto__ = p;
    console.log(obj.__proto__ === p); // true


  • However, it was never an actual property:



    > var obj = {};
    > '__proto__' in obj
    false



Subclassing Array via __proto__

The main reason why __proto__ became popular was because it enabled the only way to create a subclass MyArray of Array in ES5: Array instances were exotic objects that couldn’t be created by ordinary constructors. Therefore, the following trick was used:



function MyArray() {
var instance = new Array(); // exotic object
instance.__proto__ = MyArray.prototype;
return instance;
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.customMethod = function (···) { ··· };

Subclassing in ES6 works differently than in ES5 and supports subclassing builtins out of the box.


Why __proto__ is problematic in ES5

The main problem is that __proto__ mixes two levels: the object level (normal properties, holding data) and the meta level.


If you accidentally use __proto__ as a normal property (object level!), to store data, you get into trouble, because the two levels clash. The situation is compounded by the fact that you have to abuse objects as maps in ES5, because it has no built-in data structure for that purpose. Maps should be able to hold arbitrary keys, but you can’t use the key '__proto__' with objects-as-maps.


In theory, one could fix the problem by using a symbol instead of the special name __proto__, but keeping meta-operations completely separate (as done via Object.getPrototypeOf()) is the best approach.




The two kinds of __proto__ in ECMAScript 6

Because __proto__ was so widely supported, it was decided that its behavior should be standardized for ECMAScript 6. However, due to its problematic nature, it was added as a deprecated feature. These features reside in Annex B in the ECMAScript specification, which is described as follows:



The ECMAScript language syntax and semantics defined in this annex are required when the ECMAScript host is a web browser. The content of this annex is normative but optional if the ECMAScript host is not a web browser.



JavaScript has several undesirable features that are required by a significant amount of code on the web. Therefore, web browsers must implement them, but other JavaScript engines don’t have to.


In order to explain the magic behind __proto__, two mechanisms were introduced in ES6:



  • A getter and a setter implemented via Object.prototype.__proto__.

  • In an object literal, you can consider the property key '__proto__' a special operator for specifying the prototype of the created objects.


Object.prototype.__proto__

ECMAScript 6 enables getting and setting the property __proto__ via a getter and a setter stored in Object.prototype. If you were to implement them manually, this is roughly what it would look like:



Object.defineProperty(Object.prototype, '__proto__', {
get() {
let _thisObj = Object(this);
return Object.getPrototypeOf(_thisObj);
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError();
}
if (!isObject(this)) {
return undefined;
}
if (!isObject(proto)) {
return undefined;
}
let status = Reflect.setPrototypeOf(this, proto);
if (! status) {
throw new TypeError();
}
},
});
function isObject(value) {
return Object(value) === value;
}

The getter and the setter for __proto__ in the ES6 spec:



The property key __proto__ as an operator in an object literal

If __proto__ appears as an unquoted or quoted property key in an object literal, the prototype of the object created by that literal is set to the property value:



> Object.getPrototypeOf({ __proto__: null })
null
> Object.getPrototypeOf({ '__proto__': null })
null

Using the string value '__proto__' as a computed property key does not change the prototype, it creates an own property:



> let obj = { ['__proto__']: null };
> Object.getPrototypeOf(obj) === Object.prototype
true
> Object.keys(obj)
[ '__proto__' ]

The special property key '__proto__' in the ES6 spec:





Avoiding the magic of __proto__

Define, don’t assign

Remember that there are two ways to create own properties.


First, assigning. Use the assignment operator for a property that is not yet an own property:



obj.newProperty = 123;

In three cases, no own property newProperty is created, even if it doesn’t exist, yet:



  1. A read-only property newProperty exists in the prototype chain. Then the assignment causes a TypeError in strict mode.

  2. A setter exists in the prototype chain. Then that setter is called.

  3. A getter without a setter exists in the prototype chain. Then a TypeError is thrown in strict mode. This case is similar to the first one.


Second, defining. Use Object.defineProperty() and Object.defineProperties() to always create a new own property if it doesn’t exist yet. None of the three scenarios listed for assignment prevent that.


For more information, consult section “Properties: Definition Versus Assignment” in “Speaking JavaScript”.


Defining __proto__

In ECMAScript 6, if you define (not assign) the own property __proto__, no special functionality is triggered and the getter/setter Object.prototype.__proto__ is overridden:



let obj = {};
Object.defineProperty(obj, '__proto__', { value: 123 })

Object.keys(obj); // [ '__proto__' ]
console.log(obj.__proto__); // 123

Objects that don’t have Object.prototype as a prototype

The __proto__ getter/setter is provided via Object.prototype. Therefore, an object without Object.prototype in its prototype chain doesn’t have the getter/setter, either. In the following code, dict is an example of such an object – it does not have a prototype. As a result, __proto__ now works like any other property:



> let dict = Object.create(null);
> '__proto__' in dict
false
> dict.__proto__ = 'abc';
> dict.__proto__
'abc'

__proto__ and dict objects

If you want to use an object as a dictionary then it is best if it doesn’t have a prototype. That’s why prototype-less objects are also called dict objects. In ES6, you don’t even have to escape the property key '__proto__' for dict objects, because it doesn’t trigger any special functionality.


__proto__ as an operator in an object literal lets you create dict objects more concisely:



let dictObj = {
__proto__: null,
yes: true,
no: false,
};

Note that in ES6, you should normally prefer the built-in data structure Map to dict objects, especially if keys are not fixed.


__proto__ and JSON

Prior to ES6, the following could happen in a JavaScript engine:



> JSON.parse('{"__proto__": []}') instanceof Array
true

With __proto__ being a getter/setter in ES6, JSON.parse() works fine, because it defines properties, it doesn’t assign them (if implemented properly, an older version of V8 did assign).


JSON.stringify() isn’t affected by __proto__, either, because it only considers own properties. Objects that have an own property whose name is __proto__ work fine:



> JSON.stringify({['__proto__']: true})
'{"__proto__":true}'



Detecting support for ES6-style __proto__

Support for ES6-style __proto__ varies from engine to engine. Consult kangax’ ECMAScript 6 compatibility table for information on the status quo:



The following two sections describe how you can programmatically detect whether an engine supports either of the two kinds of __proto__.


Feature: __proto__ as getter/setter

A simple check for the getter/setter:



var supported = {}.hasOwnProperty.call(Object.prototype, '__proto__');

A more sophisticated check:



var desc = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
var supported = (
typeof desc.get === 'function' && typeof desc.set === 'function'
);

Feature: __proto__ as an operator in an object literal

You can use the following check:



var supported = Object.getPrototypeOf({__proto__: null}) === null;



Recommendations for __proto__

It is nice how well ES6 turns __proto__ from something obscure into something that is easy to understand.


However, I still recommend not to use it. It is effectively a deprecated feature and not part of the core standard. You can’t rely on it being there for code that should run on all engines.


More recommendations:



  • Use Object.getPrototypeOf() to get the prototype of an object.

  • Use Object.create() to create a new object with a given prototype. Avoid Object.setPrototypeOf(). If you change the prototype of an existing object, it can become slower.

  • I actually like __proto__ as an operator in an object literal. It is useful for demonstrating prototypal inheritance and for creating dict objects. However, the previously mentioned caveats do apply.



Comments

Popular posts from this blog

Steve Lopez and the Importance of Newspapers

Ideas for fixing unconnected computing

Omar to kill me