Enumerability is an attribute of object properties. In this chapter, we take a closer look at how it is used and how it influences operations such as Object.keys()
and Object.assign()
.
Required knowledge: property attributes
For this chapter, you should be familiar with property attributes. If you aren’t, check out §9 “Property attributes: an introduction”.
To demonstrate how various operations are affected by enumerability, we use the following object obj
whose prototype is proto
.
const protoEnumSymbolKey = Symbol('protoEnumSymbolKey');
const protoNonEnumSymbolKey = Symbol('protoNonEnumSymbolKey');
const proto = Object.defineProperties({}, {
protoEnumStringKey: {
value: 'protoEnumStringKeyValue',
enumerable: true,
},
[protoEnumSymbolKey]: {
value: 'protoEnumSymbolKeyValue',
enumerable: true,
},
protoNonEnumStringKey: {
value: 'protoNonEnumStringKeyValue',
enumerable: false,
},
[protoNonEnumSymbolKey]: {
value: 'protoNonEnumSymbolKeyValue',
enumerable: false,
},
});
const objEnumSymbolKey = Symbol('objEnumSymbolKey');
const objNonEnumSymbolKey = Symbol('objNonEnumSymbolKey');
const obj = Object.create(proto, {
objEnumStringKey: {
value: 'objEnumStringKeyValue',
enumerable: true,
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
enumerable: true,
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
enumerable: false,
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
enumerable: false,
},
});
Operation | String keys | Symbol keys | Inherited | |
---|---|---|---|---|
Object.keys() |
ES5 | ✔ |
✘ |
✘ |
Object.values() |
ES2017 | ✔ |
✘ |
✘ |
Object.entries() |
ES2017 | ✔ |
✘ |
✘ |
Spreading {...x} |
ES2018 | ✔ |
✔ |
✘ |
Object.assign() |
ES6 | ✔ |
✔ |
✘ |
JSON.stringify() |
ES5 | ✔ |
✘ |
✘ |
for-in |
ES1 | ✔ |
✘ |
✔ |
The following operations (summarized in tbl. 2) only consider enumerable properties:
Object.keys()
[ES5] returns the keys of enumerable own string-keyed properties.
Object.values()
[ES2017] returns the values of enumerable own string-keyed properties.
Object.entries()
[ES2017] returns key-value pairs for enumerable own string-keyed properties. (Note that Object.fromEntries()
does accept symbols as keys, but only creates enumerable properties.)
Spreading into object literals [ES2018] only considers own enumerable properties (with string keys or symbol keys).
Object.assign()
[ES6] only copies enumerable own properties (with either string keys or symbol keys).
JSON.stringify()
[ES5] only stringifies enumerable own properties with string keys.
for-in
loop [ES1] traverses the keys of own and inherited enumerable string-keyed properties.
for-in
is the only built-in operation where enumerability matters for inherited properties. All other operations only work with own properties.
Operation | Str. keys | Sym. keys | Inherited | |
---|---|---|---|---|
Object.getOwnPropertyNames() |
ES5 | ✔ |
✘ |
✘ |
Object.getOwnPropertySymbols() |
ES6 | ✘ |
✔ |
✘ |
Reflect.ownKeys() |
ES6 | ✔ |
✔ |
✘ |
Object.getOwnPropertyDescriptors() |
ES2017 | ✔ |
✔ |
✘ |
The following operations (summarized in tbl. 3) consider both enumerable and non-enumerable properties:
Object.getOwnPropertyNames()
[ES5] lists the keys of all own string-keyed properties.
Object.getOwnPropertySymbols()
[ES6] lists the keys of all own symbol-keyed properties.
Reflect.ownKeys()
[ES6] lists the keys of all own properties.
Object.getOwnPropertyDescriptors()
[ES2017] lists the property descriptors of all own properties.
> Object.getOwnPropertyDescriptors(obj)
{
objEnumStringKey: {
value: 'objEnumStringKeyValue',
writable: false,
enumerable: true,
configurable: false
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
writable: false,
enumerable: false,
configurable: false
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
writable: false,
enumerable: true,
configurable: false
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
writable: false,
enumerable: false,
configurable: false
}
}
Introspection enables a program to examine the structure of values at runtime. It is metaprogramming: Normal programming is about writing programs; metaprogramming is about examining and/or changing programs.
In JavaScript, common introspective operations have short names, while rarely used operations have long names. Ignoring non-enumerable properties is the norm, which is why operations that do that have short names and operations that don’t, long names:
Object.keys()
ignores non-enumerable properties.Object.getOwnPropertyNames()
lists the string keys of all own properties.However, Reflect
methods (such as Reflect.ownKeys()
) deviate from this rule because Reflect
provides operations that are more “meta” and related to Proxies.
Additionally, the following distinction is made (since ES6, which introduced symbols):
Therefore, a better name for Object.keys()
would now be Object.names()
.
In this section, we’ll abbreviate Object.getOwnPropertyDescriptor()
like this:
Most data properties are created with the following attributes:
That includes:
Object.fromEntries()
The most important non-enumerable properties are:
Prototype properties of built-in classes
Prototype properties created via user-defined classes
Property .length
of Arrays:
Property .length
of strings (note that all properties of primitive values are read-only):
We’ll look at the use cases for enumerability next, which will tell us why some properties are enumerable and others aren’t.
Enumerability is an inconsistent feature. It does have use cases, but there is always some kind of caveat. In this section, we look at the use cases and the caveats.
for-in
loopThe for-in
loop traverses all enumerable string-keyed properties of an object, own and inherited ones. Therefore, the attribute enumerable
is used to hide properties that should not be traversed. That was the reason for introducing enumerability in ECMAScript 1.
In general, it is best to avoid for-in
. The next two subsections explain why. The following function will help us demonstrate how for-in
works.
function listPropertiesViaForIn(obj) {
const result = [];
for (const key in obj) {
result.push(key);
}
return result;
}
for-in
for objectsfor-in
iterates over all properties, including inherited ones:
const proto = {enumerableProtoProp: 1};
const obj = {
__proto__: proto,
enumerableObjProp: 2,
};
assert.deepEqual(
listPropertiesViaForIn(obj),
['enumerableObjProp', 'enumerableProtoProp']);
With normal plain objects, for-in
doesn’t see inherited methods such as Object.prototype.toString()
because they are all non-enumerable:
In user-defined classes, all inherited properties are also non-enumerable and therefore ignored:
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
getName() {
return this.first + ' ' + this.last;
}
}
const jane = new Person('Jane', 'Doe');
assert.deepEqual(
listPropertiesViaForIn(jane),
['first', 'last']);
Conclusion: In objects, for-in
considers inherited properties and we usually want to ignore those. Then it is better to combine a for-of
loop with Object.keys()
, Object.entries()
, etc.
for-in
for ArraysThe own property .length
is non-enumerable in Arrays and strings and therefore ignored by for-in
:
However, it is generally not safe to use for-in
to iterate over the indices of an Array because it considers both inherited and own properties that are not indices. The following example demonstrate what happens if an Array has an own non-index property:
const arr1 = ['a', 'b'];
assert.deepEqual(
listPropertiesViaForIn(arr1),
['0', '1']);
const arr2 = ['a', 'b'];
arr2.nonIndexProp = 'yes';
assert.deepEqual(
listPropertiesViaForIn(arr2),
['0', '1', 'nonIndexProp']);
Conclusion: for-in
should not be used for iterating over the indices of an Array because it considers both index properties and non-index properties:
If you are interested in the keys of an Array, use the Array method .keys()
:
If you want to iterate over the elements of an Array, use a for-of
loop, which has the added benefit of also working with other iterable data structures.
By making properties non-enumerable, we can hide them from some copying operations. Let us first examine two historical copying operations before moving on to more modern copying operations.
Object.extend()
Prototype is a JavaScript framework that was created by Sam Stephenson in February 2005 as part of the foundation for Ajax support in Ruby on Rails.
Prototype’s Object.extend(destination, source)
copies all enumerable own and inherited properties of source
into own properties of destination
. It is implemented as follows:
function extend(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
}
If we use Object.extend()
with an object, we can see that it copies inherited properties into own properties and ignores non-enumerable properties (it also ignores symbol-keyed properties). All of this is due to how for-in
works.
const proto = Object.defineProperties({}, {
enumProtoProp: {
value: 1,
enumerable: true,
},
nonEnumProtoProp: {
value: 2,
enumerable: false,
},
});
const obj = Object.create(proto, {
enumObjProp: {
value: 3,
enumerable: true,
},
nonEnumObjProp: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
extend({}, obj),
{enumObjProp: 3, enumProtoProp: 1});
$.extend()
jQuery’s $.extend(target, source1, source2, ···)
works similar to Object.extend()
:
source1
into own properties of target
.source2
.Basing copying on enumerability has several downsides:
While enumerability is useful for hiding inherited properties, it is mainly used in this manner because we usually only want to copy own properties into own properties. The same effect can be better achieved by ignoring inherited properties.
Which properties to copy often depends on the task at hand; it rarely makes sense to have a single flag for all use cases. A better choice is to provide a copying operation with a predicate (a callback that returns a boolean) that tells it when to ignore properties.
Enumerability conveniently hides the own property .length
of Arrays when copying. But that is an incredibly rare exceptional case: a magic property that both influences sibling properties and is influenced by them. If we implement this kind of magic ourselves, we will use (inherited) getters and/or setters, not (own) data properties.
Object.assign()
[ES5]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 (with either string keys or symbol keys). Object.assign()
uses a “get” operation to read a value from a source and a “set” operation to write a value to the target.
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 theextend()
APIs already in circulation. We thought the precedent of not copying enumerable methods in those cases was enough reason forObject.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.
Cases where non-enumerability helps are few. One rare example is a recent issue that the library fs-extra
had:
The built-in Node.js module fs
has a property .promises
that contains an object with a Promise-based version of the fs
API. At the time of the issue, reading .promise
led to the following warning being logged to the console:
ExperimentalWarning: The fs.promises API is experimental
In addition to providing its own functionality, fs-extra
also re-exports everything that’s in fs
. For CommonJS modules, that means copying all properties of fs
into the module.exports
of fs-extra
(via Object.assign()
). And when fs-extra
did that, it triggered the warning. That was confusing because it happened every time fs-extra
was loaded.
A quick fix was to make property fs.promises
non-enumerable. Afterwards, fs-extra
ignored it.
If we make a property non-enumerable, it can’t by seen by Object.keys()
, the for-in
loop, etc., 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. We can therefore use enumerability to determine which own properties should be exported to JSON. This use case is similar to the previous one, marking properties as private. 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?
As an alternative to enumerability, an object can implement the method .toJSON()
and JSON.stringify()
stringifies whatever that method returns, instead of the object itself. The next example demonstrates how that works.
class Point {
static fromJSON(json) {
return new Point(json[0], json[1]);
}
constructor(x, y) {
this.x = x;
this.y = y;
}
toJSON() {
return [this.x, this.y];
}
}
assert.equal(
JSON.stringify(new Point(8, -3)),
'[8,-3]'
);
I find toJSON()
cleaner than enumerability. It also gives us more freedom w.r.t. what the storage format should look like.
We have seen that almost all applications for non-enumerability are work-arounds that now have other and better solutions.
For our own code, we can usually pretend that enumerability doesn’t exist:
That is, we automatically follow best practices.