__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); // trueHowever, 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:
- A read-only property
newProperty
exists in the prototype chain. Then the assignment causes aTypeError
in strict mode. - A setter exists in the prototype chain. Then that setter is called.
- 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. AvoidObject.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
Post a Comment