Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

12 Enumerability of properties



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”.

12.1 How enumerability affects property-iterating constructs

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,
  },
});

12.1.1 Operations that only consider enumerable properties

Table 2: Operations that ignore non-enumerable properties.
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:

for-in is the only built-in operation where enumerability matters for inherited properties. All other operations only work with own properties.

12.1.2 Operations that consider both enumerable and non-enumerable properties

Table 3: Operations that consider both enumerable and non-enumerable 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:

12.1.3 Naming rules for introspective operations

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:

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().

12.2 The enumerability of pre-defined and created properties

In this section, we’ll abbreviate Object.getOwnPropertyDescriptor() like this:

const desc = Object.getOwnPropertyDescriptor.bind(Object);

Most data properties are created with the following attributes:

{
  writable: true,
  enumerable: false,
  configurable: true,
}

That includes:

The most important non-enumerable properties are:

We’ll look at the use cases for enumerability next, which will tell us why some properties are enumerable and others aren’t.

12.3 Use cases for enumerability

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.

12.3.1 Use case: Hiding properties from the for-in loop

The 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;
}
12.3.1.1 The caveats of using for-in for objects

for-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:

const obj = {};
assert.deepEqual(
  listPropertiesViaForIn(obj),
  []);

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.

12.3.1.2 The caveats of using for-in for Arrays

The own property .length is non-enumerable in Arrays and strings and therefore ignored by for-in:

> listPropertiesViaForIn(['a', 'b'])
[ '0', '1' ]
> listPropertiesViaForIn('ab')
[ '0', '1' ]

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:

12.3.2 Use case: Marking properties as not to be copied

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.

12.3.2.1 Historical copying operation: Prototype’s 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});
12.3.2.2 Historical copying operation: jQuery’s $.extend()

jQuery’s $.extend(target, source1, source2, ···) works similar to Object.extend():

12.3.2.3 The downsides of enumerability-driven copying

Basing copying on enumerability has several downsides:

12.3.2.4 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 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.

12.3.2.5 A rare example of non-enumerability being useful when copying

Cases where non-enumerability helps are few. One rare example is a recent issue that the library fs-extra had:

12.3.3 Marking properties as private

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:

12.3.4 Hiding own properties from 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.

12.4 Conclusion

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.