Enumerability is an attribute of object properties. This blog post explains how it works in ECMAScript 6.
Let’s first explore what attributes are.
Each object has zero or more properties. Each property has a key and three or more attributes, named slots that store the data of the property (in other words, a property is itself much like a JavaScript object or a record with fields in a database).
ECMAScript 6 supports the following attributes (as does ES5):
enumerable
: Setting this attribute to false
hides the property from some operations.configurable
: Setting this attribute to false
prevents several changes to a property (attributes except value
can’t be change, property can’t be deleted, etc.).value
: holds the value of the property.writable
: controls whether the property’s value can be changed.get
: holds the getter (a function).set
: holds the setter (a function).You can retrieve the attributes of a property via Object.getOwnPropertyDescriptor()
, which returns the attributes as a JavaScript object:
> let obj = { foo: 123 };
> Object.getOwnPropertyDescriptor(obj, 'foo')
{ value: 123,
writable: true,
enumerable: true,
configurable: true }
This blog post explains how the attribute enumerable
works in ES6. All other attributes and how to change attributes is explained in Sect. “Property Attributes and Property Descriptors” in “Speaking JavaScript”.
ECMAScript 5:
for-in
loop: iterates over the string keys of own and inherited enumerable properties.Object.keys()
: returns the string keys of enumerable own properties.JSON.stringify()
: only stringifies enumerable own properties with string keys.ECMAScript 6:
Object.assign()
: only copies enumerable own properties (both string keys and symbol keys are considered).Reflect.enumerate()
: returns all property names that for-in
iterates over.for-in
and Reflect.enumerate()
are the only built-in operations where enumerability matters for inherited properties. All other operations only work with own properties.
Unfortunately, enumerability is quite an idiosyncratic feature. This section presents several use cases for it and argues that, apart from protecting legacy code from breaking, its usefulness is limited.
for-in
loop The for-in
loop iterates over all enumerable properties of an object, own and inherited ones. Therefore, the attribute enumerable
is used to hide properties that should not be iterated over. That was the reason for introducing enumerability in ECMAScript 1.
Non-enumerable properties occur in the following locations in the language:
All prototype
properties of built-in classes are non-enumerable:
> const desc = Object.getOwnPropertyDescriptor.bind(Object);
> desc(Object.prototype, 'toString').enumerable
false
All prototype
properties of classes are non-enumerable:
> desc(class {foo() {}}.prototype, 'foo').enumerable
false
In Arrays, length
is not enumerable, which means that for-in
only iterates over indices. (However, that can easily change if you add a property via assignment, which is makes it enumerable.)
> desc([], 'length').enumerable
false
> desc(['a'], '0').enumerable
true
The main reason for making all of these properties non-enumerable is to hide them (especially the inherited ones) from legacy code that uses the for-in
loop or $.extend()
(and similar operations that copy both inherited and own properties; see next section). Both operations should be avoided in ES6. Hiding them ensures that the legacy code doesn’t break.
When it comes to copying properties, there are two important historical precedents that take enumerability into consideration:
Prototype’s Object.extend(destination, source)
let obj1 = Object.create({ foo: 123 });
Object.extend({}, obj1); // { foo: 123 }
let obj2 = Object.defineProperty({}, 'foo', {
value: 123,
enumerable: false
});
Object.extend({}, obj2) // {}
jQuery’s $.extend(target, source1, source2, ···)
copies all enumerable own and inherited properties of source1
etc. into own properties of target
.
let obj1 = Object.create({ foo: 123 });
$.extend({}, obj1); // { foo: 123 }
let obj2 = Object.defineProperty({}, 'foo', {
value: 123,
enumerable: false
});
$.extend({}, obj2) // {}
Problems with this way of copying properties:
Turning inherited source properties into own target properties is rarely what you want. That’s why enumerability is used to hide inherited properties.
Which properties to copy and which not often depends on the task at hand, it rarely makes sense to have a single flag for everything. A better choice is to provide the copying operation with a predicate (a callback that returns a boolean) that tells it when to consider a property.
The only instance property that is non-enumerable in the standard library is property length
of Arrays. However, that property only needs to be hidden due to it magically updating itself via other properties. You can’t create that kind of magic property for your own objects (short of using a Proxy).
Object.assign()
In ES6, Object.assign(target, source_1, source_2, ···)
can be used to merge the sources into the target. All own enumerable properties of the sources are considered (that is, keys can be either strings or symbols). Object.assign()
uses:
let value = source[propKey]
).target[propKey] = value
).That means that both getters and setters are triggered (the former are not copied, the latter are not overridden with new properties).
With regard to enumerability, Object.assign()
continues the tradition of Object.extend()
and $.extend()
. Quoting Yehuda Katz:
Object.assign would pave the cowpath of all of the extend() APIs already in circulation. We thought the precedent of not copying enumerable methods in those cases was enough reason for Object.assign to have this behavior.
In other words: Object.assign()
was created with an upgrade path from $.extend()
(and similar) in mind. Its approach is cleaner than $.extend
’s, because it ignores inherited properties.
Note: don’t use Object.assign()
to copy methods.
Prototype methods are non-enumerable. You therefore can’t use Object.assign()
to copy methods from one prototype to another one. You could use it to copy methods from an object literal (which are enumerable) to a prototype. However, then the copied methods wouldn’t have the right enumerability. Furthermore, a method that uses super
has a property that points to the object that hosts it. Object.assign()
does not correctly update that property.
If you make a property non-enumerable, it can’t by seen by Object.keys()
and the for-in
loop, anymore. With regard to those mechanisms, the property is private.
However, there are several problems with this approach:
JSON.stringify()
JSON.stringify()
does not include properties in its output that are non-enumerable. You can therefore use enumerability to determine which own properties should be exported to JSON. This use case is similar to marking properties as private, the previous use case. But it is also different, because this is more about exporting and slightly different considerations apply. For example: Can an object be completely reconstructed from JSON?
An alternative for specifying how an object should be converted to JSON is to use toJSON()
:
let obj = {
foo: 123,
toJSON() {
return { bar: 456 };
},
};
JSON.stringify(obj); // '{"bar":456}'
I find toJSON()
cleaner than enumerability for the current use case. It also gives you more control, because you can export properties that don’t exist on the object.
In general, a shorter name means that only enumerable properties are considered:
Object.keys()
ignores non-enumerable propertiesObject.getOwnPropertyNames()
lists all property namesHowever, Reflect.ownKeys()
deviates from that rule, it ignores enumerability and returns the keys of all properties. Additionally, starting with ES6, the following distinction is made:
Therefore, a better name for Object.keys()
would now be Object.names()
.
It seems to me that enumerability is only suited for hiding properties from the for-in
loop and $.extend()
(and similar operations). Both are legacy features, you should avoid them in new code. As for the other use cases:
toJSON()
method is more powerful and explicit than enumerability when it comes to controlling how to convert an object to JSON.I’m not sure what the best strategy is for enumerability going forward. If, with ES6, we had started to pretend that it didn’t exist (except for making prototype properties non-enumerable so that old code doesn’t break), we might eventually have been able to deprecate enumerability. However, Object.assign()
considering enumerability runs counter that strategy (but it does so for a valid reason, backward compatibility).
In my own ES6 code, I’m not using enumerability, except for classes whose prototype
methods are non-enumerable.
Lastly, when using an interactive command line, I occasionally miss an operation that returns all property keys of an object, not just the own ones (Reflect.ownKeys
) or not just string-valued enumerable ones (Reflect.enumerate
). Such an operation would provide a nice overview of the contents of an object.
Feel free to disagree with what I have written in this blog post and let us know in the comments below. My opinion about enumerability is still evolving, because it’s such a confusing feature.