JavaScript properties: inheritance and enumerability

[2011-07-16] dev, javascript, jslang
(Ad, please don’t block)
Update 2012-10-29:Properties in JavaScript” is a general introduction to how properties work.

This post examines how inheritance and enumerability affect operations on properties in JavaScript.

Kinds of properties

In principle, objects in JavaScript are simple maps (dictionaries) from strings to values. However, two factors make things more complicated:
  • Own versus inherited properties. An object can point to its prototype, via an internal property. The prototype chain is the sequence of objects that starts with an object, continues with its prototype, the prototype’s prototype, etc. Many operations consider all properties in a prototype chain, some operations only consider the own properties that are stored in the first object of the prototype chain.
  • Enumerable properties. You can hide properties from some operations by making them non-enumerable. Enumerability is one of the three attributes of a property: writability, enumerability, configurability [1].

Accessing properties

There are the following operations for accessing properties:
  1. Enumerable own properties.
    Get property names:
        Object.keys(obj)
    
  2. All own properties.
    Get/detect property names:
        Object.getOwnPropertyNames(obj)
        obj.hasOwnProperty(propName)
    
    Get property value:
        Object.getOwnPropertyDescriptor(obj, propName)
    
    Set property values, delete properties (only affects the first object in the prototype chain):
        obj.propName = value
        obj["propName"] = value
    
        delete obj.propName
        delete obj["propName"]
    
        Object.defineProperty(obj, propName, desc)
        Object.defineProperties(obj, descObj)
    
  3. Enumerable inherited properties.
    Get property names:
        for (propName in obj)
    
  4. All inherited properties.
    Detect property name:
        propName in obj
    
    Read property value:
        obj.propName
        obj["propName"]
    

Using an object as a map

Objects are frequently used as maps from strings to values or a sets of strings. One has to be careful when doing so: Almost every object has the prototype Object.prototype and thus inherits many properties:
    > "valueOf" in {}
    true
    > "toString" in {}
    true
With ECMAScript 5, you use the operations from (1) and (2) and everything is OK.
    > var proto = { foo: 123 };
    > var obj = Object.create(proto);
    > obj.hasOwnProperty("foo")
    false
However, prior to ECMAScript 5, people often used the operations from (3) and (4) and that causes problems:
    > for (var p in obj) console.log(p);
    foo
    > "foo" in obj
    true
If you make the prototype property non-enumerable, you can fix the for-in loop, but not the in operator:
    > var proto = {};
    > Object.defineProperty(proto, "foo", { enumerable: false, value: 123 });
    {}
    > var obj = Object.create(proto);
    > for (var p in obj) console.log(p);
    > "foo" in obj
    true
There are more challenges when it comes to using objects as maps, consult [2] for details.

Enumerability and the standard library

In JavaScript, many properties are non-enumerable, especially all properties of prototypes. The only reason for this is to hide them from for-in. Let us examine what JavaScript hides by using the following two helper functions.
    /** Return an array with the names of the inherited enumerable properties of obj */
    function inheritedEnumerablePropertyNames(obj) {
        var result = [];
        for (var propName in obj) {
            result.push(propName);
        }
        return result;
    }

    /** Return an array with the names of the inherited properties of obj */
    function inheritedPropertyNames(obj) {
        if ((typeof obj) !== "object") { // null is not a problem
            throw new Error("Only objects are allowed");
        }
        var props = {};
        while(obj) {
            Object.getOwnPropertyNames(obj).forEach(function(p) {
                props[p] = true;
            });
            obj = Object.getPrototypeOf(obj);
        }
        return Object.getOwnPropertyNames(props);
    }
Objects: all non-own properties are non-enumerable.
    > inheritedPropertyNames({ foo: "abc" })
    [ 'foo',
      'constructor',
      'toString',
      ...
      '__lookupSetter__' ]
    > inheritedEnumerablePropertyNames({ foo: "abc" })
    [ 'foo' ]
Arrays: all non-own properties and length are non-enumerable.
    > inheritedPropertyNames([ "abc" ])
    [ '0',
      'length',
      'constructor',
      'concat',
      ...
      '__lookupSetter__' ]
    > inheritedEnumerablePropertyNames([ "abc" ])
    [ '0' ]
Note that this might give you the idea that you can use for-in to iterate over the indices in an array. However that is not recommended, because it won’t work properly if someone adds a (non-index) property to the array.

Best practices

The following recommendations involve ECMAScript 5 methods. Use a shim to get these methods in older browsers [3].

JavaScript programmers:

  • If you use an object as a map, only work with own properties, e.g. via the ECMAScript 5 method Object.getOwnPropertyNames() or via Object.prototype.hasOwnProperty().
  • Iterating over objects and arrays: see [4].
API authors:
  • When adding properties to built-in prototypes [5], use Object.defineProperty() and similar methods to make them non-enumerable. That will give you some protection against breaking for-in loops in legacy code.
  • With your own types, you don’t have to be as careful, because you can expect new code to ignore inherited properties when using objects as maps.
Future:
  • ECMAScript.next will have a dedicated type for maps. We thus won’t have to (ab)use objects as maps, any more.

Related reading

  1. John Resig - ECMAScript 5 Objects and Properties
  2. The pitfalls of using objects as maps in JavaScript
  3. es5-shim: use ECMAScript 5 in older browsers
  4. Iterating over arrays and objects in JavaScript
  5. Everything is Permitted: Extending Built-ins” [video]. Talk by Andrew Dupont at JSConf 2011. Inspired this post. Thanks to Brendan Eich for the pointer.