ES6 Destructuring In-Depth

Syntax

Destructing is a convenient way in ES6 for extracting data from data sources. Data sources can be objects and array-like objects. Destructuring has following form:

«pattern» = «data source»
  • «data source»: also known as destructuring source, is the object to be destructured.
  • «pattern»: also known as destructuring target, is the pattern for extracting data from the data source.

The «data source» can be any objects except null and undefined. Behind the scenes, the JS engine will coerce the «data source» using internal operation ToObject before destructuring accesses the «data source» properties.

«pattern» = ToObject(«data source»)

When arguments are null and undefined, ToObject will throw a TypeError exception. So, when using null and undefined as «data source»s, destructuring operation will fail before accessing properties.

The pattern can be:

  • Object pattern: { prop1: «pattern», prop2: «pattern», prop3: «pattern», …}
  • Array pattern: [«pattern», «pattern», «pattern»,…,«pattern»]
var { x: x } = null; //TypeError: Cannot match against 'undefined' or 'null'
var { y: y } = undefined; //TypeError: Cannot match against 'undefined' or 'null'

var o = {name: 'John', age: 25, title: 'Software developer'};
//ES5
var name = o.name;
var age = o.age;
var title = o.title;
console.log(name, age, title); //John 25 Software developer

//ES6
var {name: name, age: age, title: title} = o;
console.log(name, age, title); //John 25 Software developer

var arr = [10, 20, 30];
//ES5
var x = arr[0];
var y = arr[1];
var z = arr[2];
console.log(x, y, z); //10 20 30

//ES6
var [x, y, z] = arr;
console.log(x, y, z); //10 20 30

With ES6 destructuring, our code is shorter and looks much nicer. When the name of the property being extracted is the same as the variable to we want declare, we can shorten the syntax:

var {prop1, prop2, prop3} = o;
//equivalent to
var {prop1: prop1, prop2: prop2, prop3: prop3} = o;

We can rewrite above examples like this:

var o = {name: 'John', age: 25, title: 'Software developer'};
var {name, age, title} = o;

Contructuring data vs Destructuring data

The quick way to create an object in JS is to use object literal form:

var o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};

In object literal form, we know that name is the object property and the string ‘John’ is the source data that is assigned to name. The pattern here is «target» : «source». We can easily understand this because this is similar to = assignment («target» = «source»)

With ES6 destructuring, we revert the pattern «target» : «source»:

var o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
var {name: n, age: a, title: t} = o;

console.log(name, age, title); //ReferenceError
console.log(n, a, t); //John 25 Software developer

The pattern for destructuring is «source» : «target». In other words, in constructuring object with object liter form, we use «target» : «source» while in destructuring object we use «source» : «target». See the difference?

Usage

Variable declaration

We can use destructuring assignment with var, let, const like in previous examples.

let o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
let {name: n, age: a, title: t} = o;
console.log(n, a, t); //John 25 Software developer

let numbers = [1, 2, 3];
let [x, y, z] = numbers;

console.log(x, y, z); // 1 2 3

Of course, we can use shorter syntax:

let o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
let {name, age, title} = o;
console.log(name, age, title); //John 25 Software developer

Assignment

We can use destructuring with assignment as well.

let o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
let n, a, t;
({name: n, age: a, title: t} = o);
console.log(n, a, t); //John 25 Software developer

let numbers = [1, 2, 3];
let x, y, z;
[x, y, z] = numbers;

console.log(x, y, z); // 1 2 3

Note that in the first example with object destructuring, when we don’t use var, let, const, we have to wrap the whole assignment in (..) because {..} alone will create a block statement instead of an object.

Actually, the «target»s (e.g n, x, y..) in destructuring don’t need to be just variables. They can be anything that create valid assignments:

let o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
let o1 = {};
({name: o1.n, age: o1.a, title: o1.t} = o);
console.log(o1); //{n: "John", a: 25, t: "Software developer"}

let numbers = [1, 2, 3];
let nums = [];
[nums[2], nums[1], nums[0]] = numbers;

console.log(nums); // 3 2 1

let o2 = {a: 1, b: 2, c: 3};
let arr = [];
({a: arr[0], b: arr[1], c: arr[2]} = o2);

Note that the source property can be listed many times:

let o = {x: 1, y: 2, z: 3};
let {x : a, x: b, y: c} = o;

Desstructuring also works with computed property names.

let o = {x: 1, y: 2, z: 3, t: 4};
let o1 = {};
let i = 0;
let prop = "x";
({[prop]: o1[prop + i++], y: o1[prop + i++], z: o1[prop + i++], t: o1[prop + i++]} = o);

console.log(o1); // {x0: 1, x1: 2, x2: 3, x3: 4}

let arr = [1, 2, 3, 4];
let o2 = {};
i = 0;
[o2[prop + i++], o2[prop + i++], o2[prop + i++], o2[prop + i++]] = arr;

console.log(o2); //{p0: 1, p1: 2, p2: 3, p3: 4}

As you can see, we can use computed property names in both «target» ([prop + i++] and «source» ([prop]).

Pick what you need

With object destructuring and array destructuring, we don’t have to extract all properties / elements. We only just need to pick what we need.

let o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
let {name, age} = o; // name='John', age=25

Array destructuring supports a syntax of Array holes to skip elements during destructuring.

var x, y, z;
let arr = [1, 2, 3, 4, 5];
[x, y] = arr; // x=1, y=2
[x, , y, , z] = arr; // x=1, y=3, z=5
[, , , , z] = arr; // z=5
Rest operator (…)

Rest operator (…) collects the remaining elements/properties of an array / object into an array or an object.

let arr = [1, 2, 3, 4, 5];
let [x, y, ...z] = arr; // x=1, y=2, z=[3, 4, 5]

let o = {
    name: 'John',
    age: 25,
    title: 'Software developer'
};
let {name, ...otherProps} = o;// name='John', otherProps = {age: 25, title: 'Software Developer'}

Note:

  • Rest operator must be the last part of an array / object
  • Rest operator actually doesn’t work for object. However, if you’re using a transpiler like Babel, you’re covered

As rest operator collects the remaining elements / properties of an array / object, we can use a pattern too.

let arr = [1, 2, 3, 4, 5];
let [x, y, ...[z, t, e]] = arr; // x=1, y=2, z=3, t=4, e = 5

If there is no matching for the rest operator, an empty array is matched against (not null or undefined):

let arr = [1];
let [x, y, ...[z]] = arr; // x=1, y=undefined, z= []
Default values

Both object destructuring and array destructuring support default values for:

  • Variables
  • Patterns

Note: Missing and undefined will trigger default values.

Default values for variables

If a part in the destructuring target has no match in the destructuring source, it matches against its default value (if specified). Otherwise, it matches against undefined.

let [x, y = 3] = []; //x=undefined, y=3
let [x = 2, y = 3] = []; // x=2, y=3
let [x = 2, y = 3] = [undefined]; // x=2, y=3
let [x = 2, y = 3] = [1]; // x=1, y=3

let {x: x1, y: y1=3} = {}; //x1=undefined, y1=3
let {x: x1=1, y: y1=3} = {x: undefined}; //x1=1, y1=3
let {x: x1=1, y: y1=3} = {}; //x1=1, y1=3
let {x: x1=1, y: y1=3} = {x: 10}; //x1=10, y1=3
let {x: x1=1, y: y1=3} = {x: 10, y: 20}; //x1=10, y1=20

Default values can refer to other variables on the left as well:

let [x = 2, y = 3, z = y] = [1]; // x=1, y=3, z=3

let {x: x1=1, y: y1=3, z: z1=x1} = {x: 10}; //x1=10, y1=3, z1: 10

For object destructuring, we can use object shorthand syntax:

let {x=20, y=10, z=x} = {x: 10}; //x=10, y=10, z=10
Default values for patterns

We can use default values for patterns. This is a less known feature.

  • Object destructuring
{prop1: «pattern»=val1, prop2: «pattern»=val2,...} = «source»
let {x: {y: z} = {y: 10}} = {}; // z=10
let {x: {y: z} = {y: 10}} = {x: undefined}; // z=10
let {x: {y: z} = {y: 10}} = {y: {y: 100}}; // z=10
let {x: {y: z} = {y: 10}} = {x: {y: 100}}; // z=100

In the first 2 examples, as there is no property x in the source object, the pattern {y: z} matches against default value {y: 10}

  • Array destructuring
[«pattern»=val1, «pattern»=val2,...] = «source»
let [{x: y}={x: 3}] = []; // y=3
let [{x: y}={x: 3}] = [{}]; // y=undefined
let [{x: y}={x: 3}] = [{x: 10}]
Combining both of them

We can combine both of variable default values and pattern default values.

[{ prop: x=100} = {prop: 10}] = [{prop: 99}]; //x=99
[{ prop: x=100} = {prop: 10}] = []; //x=10
[{ prop: x=100} = {prop: 10}] = [undefined]; //x=10
[{ prop: x=100} = {prop: undefined}] = [undefined]; //x=100
[{ prop: x=100} = {}] = [undefined]; //x=100
[{ prop: x=100 } = {prop: 10}] = [{}]; //x=100
[{ prop: x=100 } = {prop: 10}] = [{prop1: 0}]; //x=100

Destructuring parameters

Let’s take a look array destructuring for parameters

function test([x, y]) {
    console.log(x, y);
}
test([]);// undefined undefined
test[1]);// 1 undefined
test([1, 2]);// 1 2

What if we don’t pass any parameters or we pass non iterable parameters like this test() or test(1)? A TypeError is thrown TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined. If you’re still in doubt, refer to syntax section.

We can improve this a bit by using default values for patterns:

function test([x, y]=[]) {
    console.log(x, y);
}
test();// undefined undefined

What if we want x and y to be always initialized with default values as well. Just use default values for variables:

function test([x=0, y=0]=[]) {
    console.log(x, y);
}
test();// 0 0
test([1]);// 1 0
test([1, 2]);// 1 2

Now, let’s take a look at object destructuring for parameters:

function test({x, y}) {
    console.log(x, y);
}
test();// TypeError: Cannot match against 'undefined' or 'null'
test({}};// undefined undefined
test(1);// undefined undefined
test({x: 1});// 1 undefined
test({x:1, y:2}); // 1 2

To prevent TypeError when no parameters are passed, we can use default values for patterns:

function test({x, y}={}) {
    console.log(x, y);
}
test();// undefined undefined

We can combine default values for variables and default values for patterns:

function test({x=2, y=2}={}) {
    console.log(x, y);
}
test();// 2 2
test(undefined);// 2 2
test(1);// 2 2
test({});// 2 2
test({x: 3});// 3 2
test({x:3, y: 3});// 3 3

Not just Arrays

For array destructuring, the source object can be any thing that is iterable, not just arrays:

  • Built-in iterables:
  • String
  • Array
  • TypedArray
  • Map
  • Set
  • Custom iterables
let [c1, c2, c3,,c4] = "hello";
console.log(c1, c2, c3, c4);

const map = new Map().set('k1', 'val1').set('k2', 'val2');
let [e1, e2] = map;
console.log(e1, e2);//["k1", "val1"] ["k2", "val2"]

let [[k1, v1], [k2, v2]] = map;//k1="k1", v1="val1", k2="k2", v2="val2"

for([key, val] of map) {
    console.log(`${key} -->${val}`);
}
//k1 -->val1
//k2 -->val2

Let’s define a custom iterable. To make an object iterable, we need to add [Symbol.iterator] method. [Symbol.iterator] method must return an iterator object.

let myIterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
};

For simplicity, we just borrow the [Symbol.iterator] method from Array.

[x, y, z] = myIterable;//x="a", y="b", z="c"
Advertisements

ES6 classes: The hidden truths

ES6 classes: The hidden truths

For many developers, especially those coming from traditional languages such as Java, C#, PHP, etc. and those who have a great passion for OOP paradigm, ES6 may be a huge win as the most wanted feature in Javascript finally has been out: class. But are there only roses ?

Old wine in a new bolttle

We declare a class in ES6 like below:

class A {
}

When we inspect type of A

typeof A

It will print out function. If we type above code in Chrome’s console, it will print

function class A {
}

A is just a special function in that it isn’t callable (until this point in time). Trying to invoke A() will generate ReferenceError:

Uncaught ReferenceError: A is not defined

Apparently, nothing new is shipped with ES6.

Just convenient syntax

Let’s see what the Javascript engine will do when it deals with class

class Rectangle {
    constructor(width, height) {
        this.height= height;
        this.width = width;
    }

    getArea() {
        return this.height* this.width;
    }
    toString() {
        return `Rectangle: width(${this.width}), height(${this.height})`;
    }
    static create(height, width) {
        return new Rectangle(height, width);
    }
}

class Square extends Rectangle {
    constructor(height) {
        super(height, height);
    }
    toString() {
        return `Square: length(${this.height})`;
    }
}

const s1 = new Square(10);

Below is what the javascript engine will see:

Object diagrams

Apparently, nothing new with ES6 class.

It’s just syntactic sugar on top of prototype inheritance.

An apple vs. an orange

Let’s see what are the differences between ES6 class and OOP language class.

Concept

In traditional OOP languages, class is a blueprint / template from which instances are created.

In Javascript, class is just a constructor function.

Behavior

In traditional languages, when creating instances, methods, properties, etc. are copied down from parent classes to child classes and then from the class to new instances.
OOP behavior

While in Javascript, there is no such a copy from classes to classes and from classes to instances. There are just links between objects.
JS behavior

Features

class is at heart of OOP languages. It’s no strange that OOP languages support a lot of features such as: class variable scopes, multiple inheritances, static block, nested class..

In Javascript, class is just syntactic sugar on top of prototype inheritance. Features support in class is limited. Currently, only following are supported:

  • constructor
  • instance method
  • static method

Note that class property is not supported.

So, comparing Javascript class to OOP language class is like comparing an apple to an orange.

Common Promise mistakes

Common Promise mistakes

Below are some Promise mistakes I found during a code review session:

The Promise object is used for deferred and asynchronous computations. A Promise represents an operation that hasn’t completed yet, but is expected in the future.

MDN: Promise

1. Mistake: put try/catch in promise definition

function doSomething(){
    return new Promise(function(resolve, reject){
        try {
            doSomethingSync();
            doSomethingAsync('some data', function(err, data){
                if (err){
                    return reject(err);
                };
                resolve(data);
            });
        }catch(e){
            reject(e);
        }
     });
}

Promise grabs all errors (even typo errors) by wrapping all code in a try/catch so that any exception thrown during execution will be caught and converted to a rejected promise. In doSomething function, the try/catch block is unnecessary.

Better

function doSomething(){
    return new Promise(function(resolve, reject){
        doSomethingSync();
        doSomethingAsync('some data', function(err, data){
            if (err){
                return reject(err);
            };
            resolve(data);
        });
     });
}

2. Mistake: Promise hell

Promises are one of ways to solve callback hell. But using Promises incorrectly can cause ‘Promise hell’.

authenticateUser('user1').then(function(user){
    getPosts(user).then(function(posts){
        showPosts(posts).then(function(){
            console.log('done!');
        });
    });
});

In above code, we’re trying to authenticate user ‘user1’ and then get that user’s posts and finally show these posts in the home page. Nesting getPosts and showPosts as in above code causes ‘Promise hell’. To fix this, we need to un-nesting code by returning getPosts promise from the first then and handle it by the second then..

authenticateUser('user1')
  .then(function(user){
      return getPosts(user);
  })
  .then(function(posts){
      return showPosts(posts);
  })
  .then(function(){
      console.log('done!');
  });

Even better

authenticateUser('user1')
  .then(getPosts)
  .then(showPosts)
  .then(function(){
       console.log('done!');
   });

3. Mistake: Not utilizing Promise.all

In some cases, we need to fetch some resources from the server before doing some actions with these resources. Not using Promise.all utility can cause deeply nested promises:

getProduct('p1')
  .then(function(p1){
    getProduct('p2')
      .then(function(p2) {
        getProduct('p3')
          .then(function(p3) {
            return compare(p1, p2, p3);
          });
      });
});

This code can be improved by using Promise.all

Promise.all([getProduct('p1'), getProduct('p2'), getProduct('p3')])
       .then(function(products){
          return compare(products[0], products[1], products[2]);
       });

4. Mistake: Always creating unnecessary promises

function doSomething() {
    return new Promise(function(resolve, reject) {
        fetchData('resource1')
          .then(function(resource) {
             var data = process(resource);
             resolve(data);
          })
          .catch(function(err) {
              reject(err);
          });
    });
}

In above code, main purpose of the returned Promise is to capture and return the data (and error) from fetchData. Above code can be vastly improved like this:

function doSomething() {
    return fetchData('resource1')
              .then(function(resource) {
                  var data = process(resource);
                  resolve(data);
              });
}

5. Mistake: Trying make sync → async by creating promises

function isEmail(email) {
      return new Promise(function(resolve, reject) {
          if (/\S+@\S+\.\S+/.test(email)) {
              resolve(email);
          } else {
              reject(‘Invalid email’);
          }
      });
 }
 function createUser(req, resp) {
    var user = ...;
    isEmail(user.email)
        .then(function(){
            return createUser(user);
        })
        .then(function(){
           resp.status(200).end();
        })
        .catch(function(err) {
          ...
        });
}

In the function isEmail above, Promise is overused when it is trying to make sync code –> async by creating a new Promise which makes the code slower as the email validation code is deferred to next job while the validation code can be executed immediately.

function isEmail(email) {
    return /\S+@\S+\.\S+/.test(email);
}
function createUser(req, resp) {
    var user = ...;
    if (isEmail(user.email)) {
        createUser(user)
            .then(function(){
                resp.status(200).end();
            });
    } else {
        resp.status(400).end();
    }
}