2014-12-21

ECMAScript 6: new OOP features besides classes

This blog post is outdated. Please read chapter “New OOP features besides classes” in “Exploring ES6”.


Classes [2] are the major new OOP feature in ECMAScript 6 [1]. However, it also includes new features for object literals and new utility methods in Object. This blog post describes them.

New features of object literals

Method definitions

In ECMAScript 5, methods are properties whose values are functions:

    var obj = {
        myMethod: function () {
            ···
        }
    };

In ECMAScript 6, methods are still function-valued properties, but there is now a more compact way of defining them:

    let obj = {
        myMethod() {
            ···
        }
    };

Getters and setters continue to work as they did in ECMAScript 5 (note how syntactically similar they are to method definitions):

    let obj = {
        get foo() {
            console.log('GET foo');
            return 123;
        },
        set bar(value) {
            console.log('SET bar to '+value);
            // return value is ignored
        }
    };

Let’s use obj:

    > obj.foo
    GET foo
    123
    > obj.bar = true
    SET bar to true
    true

There is also a way to concisely define properties whose values are generator functions [3]:

    let obj = {
        * myGeneratorMethod() {
            ···
        }
    };

This code is equivalent to:

    let obj = {
        myGeneratorMethod: function* () {
            ···
        }
    };

Property value shorthands

Property value shorthands let you abbreviate the definition of a property in an object literal: If the name of the variable that specifies the property value is also the property key then you can omit the key. This looks as follows.

    let x = 4;
    let y = 1;
    let obj = { x, y };

The last line is equivalent to:

    let obj = { x: x, y: y };

Property value shorthands work well together with destructuring [4]:

    let obj = { x: 4, y: 1 };
    let {x,y} = obj;
    console.log(x); // 4
    console.log(y); // 1

One use case for property value shorthands are multiple return values [4].

Computed property keys

Remember that there are two ways of specifying a key when you set a property.

  1. Via a fixed name: obj.foo = true;
  2. Via an expression: obj['b'+'ar'] = 123;

In object literals, you only have option #1 in ECMAScript 5. ECMAScript 6 additionally provides option #2:

    let propKey = 'foo';
    let obj = {
        [propKey]: true,
        ['b'+'ar']: 123
    };

This new syntax can also be combined with a method definition:

    let obj = {
        ['h'+'ello']() {
            return 'hi';
        }
    };
    console.log(obj.hello()); // hi

The main use case for computed property keys are symbols: you can define a public symbol and use it as a special property key that is always unique. One prominent example is the symbol stored in Symbol.iterator. If on object has a method with that key, it becomes iterable [3]. The method must return an iterator, which is used by constructs such as the for-of loop to iterate over the object. The following code demonstrates how that works.

    let obj = {
        * [Symbol.iterator]() { // (A)
            yield 'hello';
            yield 'world';
        }
    };
    for (let x of obj) {
        console.log(x);
    }
    // Output:
    // hello
    // world

Line A starts a generator method definition with a computed key (the symbol stored in Symbol.iterator).

New methods of Object

Object.assign(target, source_1, source_2, ···)

This method merges the sources into the target: It modifies target, first copies all enumerable own properties of source_1 into it, then all own properties of source_2, etc. At the end, it returns the target.

    let obj = { foo: 123 };
    Object.assign(obj, { bar: true });
    console.log(JSON.stringify(obj));
        // {"foo":123,"bar":true}

Let’s look more close at how Object.assign() works:

  • Both kinds of property keys: Object.assign() supports both strings and symbols as property keys.

  • Only enumerable own properties: Object.assign() ignores inherited properties and properties that are not enumerable.

  • Copying via assignment: Properties in the target object are created via assignment (internal operation [[Put]]). That means that if target has (own or inherited) setters, those will be invoked during copying. An alternative would have been to define new properties, an operation which always creates new own properties and never invokes setters. There originally was a proposal for a variant of Object.assign() that uses definition instead of assignment. That proposal has been rejected for ECMAScript 6, but may be reconsidered for later editions.

Use cases for Object.assign()

Let’s look at a few use cases. You can use Object.assign() to add properties to this in a constructor:

    class Point {
        constructor(x, y) {
            Object.assign(this, {x, y});
        }
    }

Object.assign() is also useful for filling in defaults for missing properties. In the following example, we have an object DEFAULTS with default values for properties and an object options with data.

    const DEFAULTS = {
        logLevel: 0,
        outputFormat: 'html'
    };
    function processContent(options) {
        options = Object.assign({}, DEFAULTS, options); // (A)
        ···
    }

In line A, we created a fresh object, copied the defaults into it and then copied options into it, overriding the defaults. Object.assign() returns the result of these operations, which we assign to options.

Another use case is adding methods to objects:

    Object.assign(SomeClass.prototype, {
        someMethod(arg1, arg2) {
            ···
        },
        anotherMethod() {
            ···
        }
    });

You could also assign functions, but then you don’t have the nice method definition syntax and need to mention SomeClass.prototype each time:

    SomeClass.prototype.someMethod = function (arg1, arg2) {
        ···
    };
    SomeClass.prototype.anotherMethod = function () {
        ···
    };

One last use case for Object.assign() is a quick way of cloning objects:

    function clone(orig) {
        return Object.assign({}, orig);
    }

This way of cloning is also somewhat dirty, because it doesn’t preserve the property attributes of orig. If that is what you need, you have to use property descriptors.

If you want the clone to have the same prototype as the original, you can use Object.getPrototypeOf() and Object.create():

    function clone(orig) {
        let origProto = Object.getPrototypeOf(orig);
        return Object.assign(Object.create(origProto), orig);
    }

Object.getOwnPropertySymbols(obj)

In ECMAScript 6, the key of a property can be either a string or a symbol. There are now five tool methods that retrieve the property keys of an object obj:

  • Object.keys(obj)Array<string>
    retrieves all string-valued keys of all enumerable own properties.

  • Object.getOwnPropertyNames(obj)Array<string>
    retrieves all string-valued keys of all own properties.

  • Object.getOwnPropertySymbols(obj)Array<symbol>
    retrieves all symbol-valued keys of all own properties.

  • Reflect.ownKeys(obj)Array<string|symbol>
    retrieves all keys of all own properties.

  • Reflect.enumerate(obj)Iterator
    retrieves all string-values keys of all enumerable properties.

Object.is(value1, value2)

The strict equals operator (===) treats two values differently than one might expect.

First, NaN is not equal to itself.

    > NaN === NaN
    false

That is unfortunate, because it often prevents us from detecting NaN:

    > [0,NaN,2].indexOf(NaN)
    -1

Second, JavaScript has two zeros, but strict equals treats them as if they were the same value:

    > -0 === +0
    true

Doing this is normally a good thing.

Object.is() provides a way of comparing values that is a bit more precise than ===. It works as follows:

    > Object.is(NaN, NaN)
    true
    > Object.is(-0, +0)
    false

Everything else is compared as with ===.

If we combine Object.is() with the new ECMAScript 6 array method findIndex() [5], we can find NaN in arrays:

    > [0,NaN,2].findIndex(x => Object.is(x, NaN))
    1

Object.setPrototypeOf(obj, proto)

This method sets the prototype of obj to proto. The non-standard way of doing so in ECMAScript 5, that is supported by many engines, is via assinging to the special property __proto__. The recommended way of setting the prototype remains the same as in ECMAScript 5: during the creation of an object, via Object.create(). That will always be faster than first creating an object and then setting its prototype. Obviously, it doesn’t work if you want to change the prototype of an existing object.

References

  1. Using ECMAScript 6 today
  2. ECMAScript 6: classes
  3. Iterators and generators in ECMAScript 6
  4. Multiple return values in ECMAScript 6
  5. ECMAScript 6’s new array methods

No comments: