December 27, 2015

Destructuring Assignment in ECMAScript 2015

This blog post is about an interesting new feature that ECMAScript 2015 brings along. In my opinion it is really great to use, because it makes code easier to read and simplifies a lot of tasks that are usually harder to achieve.

Destructuring assignment was introduced in ECMAScript 2015. This expression allows you to get data from an object or array by mirroring the creation of an object or array. It therefore gives you the possibility to work with parts of objects using simpler and clearer syntax. You can work with all kinds of objects, such as simple objects which consist of primitive values or more complex, nested objects.

In the following sections, I will provide you with examples to all the possibilites how to use the destructuring assignment, which will make understanding the feature much easier. The list of all the possibilites is taken from the ES2015 compatibility table [1].



Features


Arrays

Traditionally, if you want to access individual parameters of an array you have to assign each array position to a single variable, as can be seen in the following example:

Conventionally:
var someArray = [1, 2, 3];
var a = someArray[0], 
    b = someArray[1], 
    c = someArray[2];
console.log(a + ' ' + b + ' ' + c);
//OUTPUT: 1 2 3

Using destructuring assignment:
var [a, b, c] = someArray;
console.log(a + ' ' + b + ' ' + c);
//OUTPUT: 1 2 3

Sparse arrays

In sparse arrays the length of an array can be larger than the number of elements that the array consists of. [2] 
Thus, you can create an array of size 3, with no elements in it. If you want to destructure it, the variables are assigned the status “undefined”.

Example:
var sparseArray = new Array(3);
var [a, , c] = sparseArray;
console.log(a + ' ' + c);
//OUTPUT undefined undefined

Strings

It's possible to destructure strings too. Because strings are forced to beeing an object during destructuring.

Example:
var [a, b, c] = 'abc';
console.log(a + ' ' + b + ' ' + c);
//OUTPUT: a b c

Astral plane strings

The planes that the code points from U+010000 to U+10FFFF belong to are called astral planes. To represent an astral code point, there are more than 4 hexadecimal digits necessary. If you need more than 4 hexadecimal digits to represent the code point, it's an astral code point. [7]

Example:
var a, b;
[a, b] = "\u{41}\u{2603}";
console.log(b);
//OUTPUT: ☃

Generator instances

"In JavaScript, generators are functions which can be exited and later re-entered. Their context will be saved across re-entraces." [3]
Generator functions need the asterisk after the function keyword. The yield keyword forces the generator to pause and to return the value to the the generators caller.

Example:
function* doSomething(){
    var count = 1;
    while(true){
        yield count++;
    }
}

var [a, b, c] = doSomething();
console.log(a + ' ' + b + ' ' + c);
//OUTPUT: 1 2 3     

Generic iterables

"The iterable protocol allows JavaScript objects to define or customize their iteration behaviour, such as what values are looped over in a “for .. of” construct." [4]
Example:
function* createGenericIterator(someArray){
    var index = 0;
    
    while(index < someArray.length){
        yield someArray[index++];
    }
}

var [a, b] = createGenericIterator(['Hello', 'World']);
console.log(a + ' ' + b);
//OUTPUT: HELLO WORLD


Instances of generic iterables

If you want to use a custom iterator of an object, you have to use “Symbol.iterator” which will overwrite the default iterator of this object.

Example:
var myIterable = {};
var count = 0;
myIterable[Symbol.iterator] = function* () {
    while(count < 3){
        yield count++;
    }
};

for (var value of myIterable) {
    console.log(value);
}
//OUTPUT: 1 2 3

Iterator closing

The return function is optional and helps to clean up the iterator in case it hasn't finished iterating. The return function will only be called if the “for” loop exits for some reason - this can be because of a break, throw, return or even a continue when it happens in an inner loop (as it then acts as a break). [5]

Example:
function getFirstLine(param) {
    var index = 0;
    return {
        next: function() {
            if(index < 1){
                return { value: param[index++], done: false };  
            }
        },
        return: function() {                
            return { value: param[index++], done: true };
        },
    };
}

for (var line of getFirstLine(['Hello', 'World'])) {
    console.log(x);
    break;
}


Iterable and Object destructuring expression

Iterable Example 1:
var x, y, myIterable = [];
myIterable[Symbol.iterator] = function* () {
    var count = 0;
    while(count < 2){
        yield count++;
    }
};
var myArray = Array.from(myIterable);
console.log(([x,y] = myArray) === myArray);
//OUTPUT: true
Object Example:
var x, y, myIterable = {};
myIterable[Symbol.iterator] = function* () {
var count = 0;
    while(count < 2){
        if(count === 0){
            yield {x: 1};
        }
        if(count === 1){
            yield {y: 2};
        }
        count++;
    }
};
console.log(([x,y] = myIterable) === myIterable);
//OUTPUT: true

Trailing commas in iterable patterns and in object patterns

It is possible to skip elements by using trailing commas. In this example we're skipping the element in the second position. If there are more variables on the left side than on the right side, they will be assigned the status "undefined".

Iterable Example:
var tmp, tmp2;
[tmp,,tmp2] = [1,2,3];
console.log(tmp + ' ' + tmp2);
//OUTPUT: 1 3
Object Example:
var tmp, someVar;
({tmp,} = {tmp: true, someVar: 1});
console.log(tmp + ' ' + someVar);
//OUTPUT true undefined

Objects

There are multiple ways to destructure an object. One way, for instance, is just to use the same variable names on the left-hand side as in the object.

Example:
var someObject = {a: 20, b: false};
var {a, b} = someObject;
console.log(a+ ' ' + b);
//OUTPUT 20 false

Alternatively, you can define different names for the variables during the process of destructuring to work with in advance, like this:

Example:
var someObject = {a: 20, b: false};
var {a: age, b: isAllowedToDrive} = someObject;
console.log(age + ' ' + isAllowedToDrive);
//OUTPUT 20 false

Object destructuring with primitives

Here, the primitive value is forced onto an object before its properties can be accessed.

Example:
var {toString: s} = 123;
console.log(s);
//OUTPUT 123

Parenthesised left-hand-side is a syntax error

In a destructured assignment expression, the parenthesis needs to be applied on both sides of the expression and not just on the left-hand side.

Example:
var x, y;
({x,y}) = {x: true, y: false};
//OUTPUT: ReferenceError: invalid assignment lefthand side

Chained object destructuring

As one can see in the example below, it is also possible to chain the object destructuring.

Example:
({a,b} = {c,d} = {a:1,b:2,c:3,d:4});
console.log(a + ' ' + b + ' ' + c + ' ' + d);
//OUTPUT: 1 2 3 4

Throws on null and undefined

It is not possible to destructure null and undefined values, as you will get a TypeError for doing so.

Example:
var someVar, someVarAgain;
({someVar} = undefined);
({someVarAgain} = null);
//OUTPUT: TypeError: can't convert undefined to object

Computed properties

The property keys can be dynamically generated. All you have to do is to enclose them in square brackets.

Example:
const KEY = 'someKey';
var output;
({[KEY]: output} = {someKey: 'Hello World'});
console.log(output);
//OUTPUT: Hello World

Nested

It is possible to nest parameters in a destructured assignment. If you have an array inside the object, you have to give it a name. Otherwise you will receive an invalid property error.

Example:
var a, b, c;
var obj = {a, something: [{someVar: b, someBoolean: c}]} = {a: 10, something: [{someVar: 3, someBoolean: true}]};
console.log(a + ' ' + b + ' ' +c);
//OUTPUT: 10 3 true

Rest

A Rest parameter must be the last variable on your left-hand side and can only be used in an array. It will be assigned to all the remaining variables on the right-hand side.

Example:
var x,y,z;
[x, ...z] = [1,2,3,4,5,6,7,8];
console.log(x + ' ' + typeof z + ' ' + z);
//OUTPUT 1 object 2,3,4,5,6,7,8

Nested rest

The nested rest is currently only implemented in JavaScript compilers like Babel or Traceur.

Example:
var a = [1, 2, 3], start, end;
[start, ...[a[2], end]] = a;
console.log(start + ' ' + a + ' ' + end);
//OUTPUT 1 2 3


Empty patterns

As can be seen in the example below, destructuring objects into empty arrays or objects is supported.

Example:
[] = [1, 2];
({} = {a: 1});

However, you can't destructure empty objects themselves as they are not iterable. Attempting this will result in a TypeError.


Example:
[] = {};

Defaults

Lastly, it is possible to define default values. You can, for example, use your destructuring source as parameters in a function like this:

Example:
function doSomething({someVar: someVar = 'Hello World', anotherVar}){
    console.log(someVar + ' ' + anotherVar);
}
//OUTPUT: Hello World undefined


This, however, is currently only implemented in Babel and Traceur – which is a real shame because it is an extremely useful function. Using defaults, you don't have to worry about the order in which you pass the parameters on to the function. All you have to do is to provide the object with the corresponding variable names and all the magic happens by itself.

When to use destructured assignment? [6]

  • Firstly, it is great when using it for function parameters because you aren't limited to the order of the function parameters - you can just pass on an object with the same variable names as the function's, and the mapping is done by destructuring.
  • In addition, you can use default parameter values, which is really great. Not making use of this functionality means continually checking for undefined variables, which makes code more difficult to read.
  • Using destructuring, you have the ability to return multiple return values from a function. You simply have to return an array or object of values and then you can destructure it at your function call.

Browser Support

If you want to make use of the full functionality spectrum, you have to use a JavaScript compilers like Babel (https://babeljs.io/) or Traceur (https://github.com/google/traceur-compiler).
Safari 9 and Firefox 34+ provide almost complete implementations  (except for the nested rest and defaults features).
At the moment, most of the major browsers such as Chrome, Edge or Opera do not support implementation of destructured assignment at all.


References

[1]: ECMAScript 6 compatibility table: https://kangax.github.io/compat-table/es6/
[2]: JavaScript the definitive guide, 6th edition: https://www.safaribooksonline.com/library/view/javascript-the-definitive/9781449393854/ch07s03.html
[3]: MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
[4]: MDN: https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Iteration_protocols
[5]: Destructuring and parameter handling in ECMAScript 6: http://www.2ality.com/2015/01/es6-destructuring.html
[6]: Hacks Mozilla: https://hacks.mozilla.org/2015/05/es6-in-depth-destructuring/
[7]: JavaScript has a Unicode problem: https://mathiasbynens.be/notes/javascript-unicode

4 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. The content is very essentials for getting destructing the assignment for the students. The best essay online is helps for making valuable writing papers by the way of online.

    ReplyDelete
  4. Your given script is very helpful for me because i am IT student and doing my internship and your this code would helpful for me. Thanks for sharing with us. Coursework Writing Services

    ReplyDelete