2015-10-05

Enumerability in ECMAScript 6

Enumerability is an attribute of object properties. This blog post explains how it works in ECMAScript 6.

Let’s first explore what attributes are.

Property attributes

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

  • All properties have the attributes:
    • 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.).
  • Normal properties (data properties, methods) have the attributes:
    • value: holds the value of the property.
    • writable: controls whether the property’s value can be changed.
  • Accessors (getters/setters) have the attributes:
    • 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”.

Constructs affected by enumerability

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.

Use cases for enumerability

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.

Use case: Hiding properties from the 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-enumerability in the language

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.

Use case: Marking properties as not to be copied

Historical precedents

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

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

  • Reading a value from a source: normal “get” operation (let value = source[propKey]).
  • Writing a value to the target: normal “set” operation (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.

Marking properties as private

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:

  • When copying an object, you normally want to copy private properties. That clashes making properties non-enumerable that shouldn’t be copied (see previous section).
  • The property isn’t really private. Getting, setting and several other mechanisms make no distinction between enumerable and non-enumerable properties.
  • When working with code either as source or interactively, you can’t immediately see whether a property is enumerable or not. A naming convention (such as prefixing property names with an underscore) is easier to discover.
  • You can’t use enumerability to distinguish between public and private methods, because methods in prototypes are non-enumerable by default.

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

Naming inconsistencies

In general, a shorter name means that only enumerable properties are considered:

  • Object.keys() ignores non-enumerable properties
  • Object.getOwnPropertyNames() lists all property names

However, 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:

  • Property keys are either strings or symbols.
  • Property names are only strings.

Therefore, a better name for Object.keys() would now be Object.names().

Looking ahead

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:

  • I don’t think there is a need for a general flag specifying whether or not to copy a property.
  • Non-enumerability does not work well as a way to keep properties private.
  • The 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.

No comments: