2015-08-20

Five little-known facts about ES5 object literals

This blog post describes five little known facts about ECMAScript 5 (ES5) object literals:

  1. ECMAScript 5 has getters and setters
  2. Trailing commas are legal
  3. You often don’t have to quote property keys
  4. You can use reserved words as unquoted property keys
  5. Using objects as dictionaries is surprisingly tricky

ECMAScript 5 has getters and setters

ECMAScript 5 supports getters and setters. You can create them either via property descriptors or via object literals:

    var obj = {
        get foo() {
            return Math.random();
        },
        set foo(value) {
            console.log('SET foo = '+value);
        },
    };

Let’s use obj in a REPL:

    > obj.foo
    0.6029663002118468
    > obj.foo
    0.99780507478863
    > obj.foo = 123;
    SET foo = 123

Starting with ES5, it is legal to put a comma after the last property. That helps whenever you want to rearrange properties, especially if there is one property per line:

    var translations = {
        yes: 'ja',
        no: 'nein',
        perhaps: 'vielleicht',
    };

You often don’t have to quote property keys

The grammar rule for PropertyName in the ES5 specification states that a property name in an object literal is either:

  • an identifier,
  • a string literal,
  • or a numeric literal.

String literals as property names

You can use arbitrary text as a property key if you quote it:

    var obj = {
        '#§$%': true,
        " \t\n": true,
    };

Identifiers as property names

If the property key is an identifier, it doesn’t have to be quoted in object literals. Identifiers must start with a Unicode letter, an underscore or a dollar sign.

The ES5 spec describes Unicode letters as:

Any character in the Unicode categories “Uppercase letter (Lu)”, “Lowercase letter (Ll)”, “Titlecase letter (Lt)”, “Modifier letter (Lm)”, “Other letter (Lo)”, or “Letter number (Nl)”.

Thus, the following property names don’t have to be quoted:

    var obj = {
        π: true,
        привет: true,
        café: true,
        $$$: true,
    };

Numbers as property names

You don’t have to quote property names in object literals if they are numeric literals. Numeric literals include the hexadecimal notation, but not a leading minus (-, a dash), because that is not part of a numeric literal in JavaScript, it is an operator.

    var obj = {
        1e2: true,
        1e-2: true,
        .234: true,
        0xFF: true,
    };

The numbers represented by these numeric literals are converted to strings before they become property keys:

    > Object.keys(obj)
    [ '100', '255', '0.01', '0.234' ]

In contrast to identifiers, numeric literals cannot be used after the dot operator:

    > obj.0xFF
    SyntaxError: Unexpected token
    > obj[0xFF]
    true

Recommendations

My recommendations (for ES5 and later):

  • For objects holding code, I only use identifiers as property names and never quote.
  • For objects holding data, I often quote property names.
  • The JSON data format requires you to quote property names – with double quotes!

        > JSON.parse('{ hello: 3 }')
        SyntaxError: Unexpected token h
        > JSON.parse("{ 'hello': 3 }")
        SyntaxError: Unexpected token '
        > JSON.parse('{ "hello": 3 }')
        { hello: 3 }
    

You can use reserved words as unquoted property keys

In ECMAScript 3, you had to quote reserved words such as instanceof or new if you wanted to use them as property names:

    > var obj = { 'new': 123 };
    > obj['new']
    123

In ECMAScript 5, that is not necessary, anymore:

    > var obj = { new: 123 };
    > obj.new
    123

Using objects as dictionaries is surprisingly tricky

ECMAScript 5 does not have a built-in data structure for dictionaries (which are also known as maps). Therefore, the programming construct object is a abused as a dictionary from strings to arbitrary values. In addition to keys having to be strings, that causes three problems.

First, You cannot invoke methods on objects-as-dictionaries. The name of any method you invoke might be a key in the data:

    > var data = { hasOwnProperty: true };
    > data.hasOwnProperty('someKey')
    TypeError: boolean is not a function

Second, you must be careful about inheritance. Getting the value of a property and the in operator include inherited properties, which is not what you want in this case:

    > var data = {}; // empty dictionary
    > 'toString' in data
    true
    > data.toString
    [Function: toString]

Third, the property key __proto__ triggers special behavior in many engines, which is why you can’t use it as a key for data. One work-around is to escape such keys (and their escaped versions):

    function escapeKey(key) {
        // Escape '__proto__', its escaped version etc.
        if (key.indexOf('__proto__') === 0) {
            return key+'%';
        } else {
            return key;
        }
    }
    function getValue(obj, key) {
        return obj[escapeKey(key)];
    }
    function setValue(obj, key, value) {
        obj[escapeKey(key)] = value;
    }

The dict pattern

An object works best as a dictionary if its prototype is null (the so-called dict pattern). That fixes problem 2:

    > var dict = Object.create(null);
    > dict.foo = 123;
    > 'toString' in dict
    false
    > dict.toString
    undefined
    > 'foo' in dict
    true

Problems 1 and 3 remain: you can’t invoke any methods on dict (it doesn’t inherit any, anyway, because it has no prototypes) and you must escape '__proto__'.

ECMAScript 6 has the built-in data structure Map which you should always use for data, especially if it has arbitrary keys.

More information on the dict pattern: section “The dict Pattern: Objects Without Prototypes Are Better Maps” in “Speaking JavaScript”.

Further reading on object literals

No comments: