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

Some techniques with ES6 default parameter values, spread and rest

Default behavior

Default parameter values let function parameters be initialized with default values when no value or undefined is passed.

function join(arr=[], sep=','){
    return arr.join(sep);
}

join();//""
join([1,2,3]); //"1,2,3"
join(["javascript", "is", "awesome"], " "); //"javascript is awesome"

We can also specify a function as a default value.

import rp from 'request-promise';

function jsonParser(body, response) {
    if (/^application\/json.*/i.test(response.headers['content-type'])){
        return JSON.parse(body);
    }
    return body;
}
function fetch(url, transform=jsonParser) {
    return rp({
        url: url,
        transform: jsonParser
    });
}
Required parameters

Another technique with default parameter values is to allow a function to declare required parameters (thanks to ). This is really useful when designing APIs that need parameter validation.

function required(param){
    throw new Error(`${param} is required`);
}
const Storage = {
    setItem: function setItem(key = required('key'), value=required('value')){
           //implentation code goes here
    },
    getItem: function getItem(key = required('key')){
    }    
}

Storage.setItem();//Uncaught Error: key is required
Storage.setItem('key1');//Uncaught Error: value is required
Storage.setItem('key1', 'value1'); //OK
Copy arrays and modify them

In ES5, we can use Array#concat or Array#slice to make a copy of an array.

var arr = [1, 2, 3, 4, 5];
var arr2 = arr.slice(0); //1, 2, 3, 4, 5
var arr3 = [].concat(arr); //1, 2, 3, 4, 5

In ES6, copying an array is even easier with spread operator.

const arr = [1, 2, 3, 5, 6];
const arr2 = [...arr]; //1, 2, 3, 5, 6
const b = [...arr.slice(0, 3), 4, ...arr.slice(3)];//1, 2, 3, 4, 5, 6
Copy objects and modify them

In ES5, we can borrow a utility function such as jQuery#extend, _.assign to make a copy of an object:

var o = {
    name: 'John',
    age: 30,
    title: 'Software Engineer',
}
var o2 = _.assign({}, o);
var o3 = _.assign({}, o, {age: 25});

In ES6, we can use the built-in function Object.assign:

const o = {
    name: 'John',
    age: 30,
    title: 'Software Engineer',
}
const o2 = Object.assign({}, o);
const o3 = Object.assign({}, o, {age: 25});

Another way is to use spread (…) operator:

const o2 = {
    ...o
}
const o3 = {
    ...o,
    age: 25
}

Note: spread operator for objects isn't supported in ES6. Hopefully, this will be included in ES7. If you're using a transpiler like Babel, you're covered.

Avoid Function.prototype.apply

Some functions such as Math.max, Math.min, Date, etc. require a list of parameters.

Math.max(1, 100, 90, 20);
new Date(2016, 7, 13);

What if we have a list of parameter values contained in an array ? A workaround is to use Function.prototype.apply(thisArg, [])

var numbers = [1, 100, 90, 20];
Math.max.apply(null, numbers); // 100

In ES6, this can be solved easily with spread operator:

var numbers = [1, 100, 90, 20];
Math.max(...numbers);

var parts = [2016, 7, 13];
var d = new Date(...parts);
Forget arguments

arguments is an array-like object that is available inside function calls. It represents the list of arguments that were passed in when invoking the function. There're some gotchas:

  • arguments is not an array. We need to convert it to an array if we want to use array methods such as slice, concat, etc.
  • arguments may be shadowed by function parameter or a variable with the same name.
function doSomething(arguments) {
    console.log(arguments);
}
doSomething(); //undefined
doSomething(1); //1

function doSomething2() {
    var arguments = 1;
    console.log(arguments);
}
doSomething2();// 1
doSomething2(2, 3, 4); // 1

In ES6, we can completely forget arguments. With rest(…) operator, we can collect all arguments passed function calls:

function doSomething(...args) {
    console.log(args);
}

doSomething(1, 2, 3, 4); //[1, 2, 3, 4]

With rest operator, all arguments passed to doSomething are collected into args. More than that, args is an array, so we don't need an extra step for converting to an array as we did for arguments.

All in one

In this part, we will use techniques above in a complex case. Let's implement the fetch API. For simplicity, we build the API on top of request-promise module.

function fetch(url, options){

}

The first step is parameter checking:

//ES5
import rp from 'request-promise';
function fetch(url, options){
    var requestURL = url || '';
    var opts = options || {};
    ...
}
//ES6
import rp from 'request-promise';
function fetch(url='', options={}){
   ...
}

We also need to check some properties of options object:

function jsonParser(body, response) {
    if (/^application\/json.*/i.test(response.headers['content-type'])){
        return JSON.parse(body);
    }
    return body;
}
//ES5
import rp from 'request-promise';
function fetch(url, options){
    var requestURL = url || '';
    var opts = options || {};
    var method = options.method || 'get';
    var headers = opts.headers || {'content-type': 'application/json'};
    var transform = jsonParser;
    ...
}
//ES6
import rp from 'request-promise';
function fetch(url='', {method='get',
                        headers={'content-type': 'application/json'},
                        transform=jsonParser}){

}

In the ES6 version of the API, we use destructuring to extract some properties (method, headers and transform) and set some default values. This doesn't work if we don't pass the options object because we can't match an pattern against undefied:

fetch();//TypeError: Cannot match against 'undefined' or 'null'

This can be fixed by a default value:

//ES6
import rp from 'request-promise';
function fetch(url='', {method='get',
                        headers={'content-type': 'application/json'},
                        transform=jsonParser} = {}){
    return rp({
        url: url,
        method: method,
        headers: headers,
        transform: transform
    });
}

As client code may pass properties other than method, headers and transform, we need to copy all remaining properties:

//ES5
import rp from 'request-promise';
function fetch(url, options){
    var requestURL = url || '';
    var opts = options || {};
    var method = options.method || 'get';
    var headers = opts.headers || {'content-type': 'application/json'};
    var transform = jsonParser;
    //copy all properties and then overwrite some
    opts = _.assign({}, opts, {method: method, headers: headers, transform: transform})

    return rp(opts);
}

In ES6, we need to collect remaining properties by rest operator:

function fetch(url='', {method='get',
                        headers={'content-type': 'application/json'},
                        transform=jsonParser,
                        ...otherOptions} = {}){

}

and using spread operator to pass those properties to the target function:

function fetch(url='', {method='get',
                        headers={'content-type': 'application/json'},
                        transform=jsonParser,
                        ...otherOptions} = {}){
    return rp({
        url: url,
        method: method,
        headers: headers,
        transform: transform,
        ...otherOptions
    });   
}

And finally, with object literal shorthand, we can write this:

function fetch(url='', {method='get',
                        headers={'content-type': 'application/json'},
                        transform=jsonParser,
                        ...otherOptions} = {}){
    return rp({
        url,
        method,
        headers,
        transform,
        ...otherOptions
    });   
}

ES6 Arrow Functions In Depth

Arrow functions are a new feature in ES6 for writing functions. This is one of the most favorite features in ES6 (see here). In this post, we’re going to learn this new feature.

Syntax

Arrow functions, also known as fat arrow functions are functions that are defined with new syntax that use an arrow (=>):

(param1, param2, …, paramN) => { statements }

const multiply = (x,y)=> {return x*y}

Parentheses are optional when there’s only one parameter:

(param)=> { statements }//is equivalent to
param=> { statements }

const square = (x)=> {return x*x}
//is equivalent to
const square = x=> {return x*x}

However, when there are no parameters, parentheses are required:

()=> { statements }

When an expression (which produces value) is the body of an arrow function, braces are not needed:

(param1, param2, …, paramN)=> {return expression}
//is equivalent to
(param1, param2, …, paramN)=> expression


const multiply = (x,y)=> {return x*y}
//is equivalent to
const multiply = (x,y)=> x*y

Note: There must be no line break between the parameters and the arrow.

(param1, param2,..,paramN)
=> { statements } // Uncaught SyntaxError: Unexpected token =>
Lexically bound: this, super, new.target and arguments

In normal functions, this is dynamically bound depending on how functions are called.

const Utils = {
  addAll: function(...) {
    ..
    this.add(..);
  },
  add: function(..) {
  }
}
...
Utils.addAll(...);//OK

let addAll= Utils.addAll;

addAll(...);// TypeError: this.updateUI is not a function(…)

Due to this behavior, it’s very easy to lose track of this inside a function. In these cases, we need to workaround.

  • Work around 1: self = this
const Utils = {
  fetch: function(...) {
    var self = this;
    return $.ajax({
      ...
      success: function(){
        self.updateUI(..);
      }
    });
  },
  updateUI: function(..) {
  }
}
  • Work around 2: Function.prototype.bind
const Utils = {
  fetch: function(...) {    
    return $.ajax({
      ...
      success: function(...){
        this.updateUI(..);
      }.bind(this)
    });
  },
  updateUI: function(..) {
  }
}

And the solution for this problem in ES6 is arrow functions.

const Utils = {
  fetch: function(...) {    
    return $.ajax({
      ...
      success: ()=>this.updateUI(..)      
    });
  },
  updateUI: function(..) {
  }
}

In addition to this, following objects are also bound lexically:

  • arguments
  • super
  • new.target
Anonymous function expressions

Arrow functions are of course functions:

const f = ()=>{}
typeof(f) //"function"
f instanceof Function // true

But they are function expressions. More than that, they don’t have name. Thus, they are anonymous function expressions. And because of this, arrow functions shouldn’t be used where we need function name for recursion and event binding.

Can’t be used as constructors

Arrow functions can’t be used as constructor. Using arrow functions as constructors will throw errors:

const f = ()=>{};
new f(); //TypeError: f is not a constructor
No prototype, arguments and caller

As arrow functions can’t be used as constructors, there is no need for prototype to be available. Arrow functions also don’t have arguments and caller.

const f = ()=>{};
Object.getOwnPropertyNames(f); //["length", "name"]

const f0 = function(){}
Object.getOwnPropertyNames(f0); //["length", "name", "arguments", "caller", "prototype"]
this can’t be changed

Arrow functions are “hard” bound. We can’t change this value.

const f = ()=>console.log(this);
f();//window object
f.bind({});
f();//window object
f.call({});//window object
f.apply({});//window object
Can’t be used as generators

In arrow functions, the keyword yield can’t be used(except when normal functions nested in it). Therefore, arrow functions can’t be used as generators.

When to use / not to use
  • Arrow functions should be used when we need a short function expression and that function expression doesn’t rely on this. Using arrow functions with Array methods is a great example:
[1, 10, 2, 9, 3].sort((a,b)=>a-b); //[1, 2, 3, 9, 10]
  • Arrow functions are best suited for non-method functions.
const Utils = {

  addAll: function(...) {
    ..
    this.add(..);
  },

  add: function(..) {
  },

  addFirst: (..)=> {
    ..
    this.add(..);
  }
}
..
Utils.addAll(); // OK

Utils.addFirst(); //  TypeError: this.add is not a function

The call Util.addAll() is fine because addAll is a normal function which is this aware (bound dynamically). Although we invoke as Utils.addFirst(), the reference to this.add(..) fails because this doesn’t point to Utils as in normal functions. It inherits this from the surrounding scope. Thus, arrow functions should not be used as method function.

  • Arrow functions can be used as nested functions which rely on var self = this; or .bind(this)
  • In some cases, we have inner functions that reply on arguments object from the enclosing function, arrow functions are a best choice.
function list() {
  const args = Array.prototype.slice.call(arguments);
  const doSomething = function() {
    ...
    console.log(args);
  }
  doSomething();
}

// Better
function list() {
  const doSomething =()=>{
    ...
    console.log(arguments);
  }
  doSomething();
}

That is because inside arrow functions arguments object inherits from the enclosing functions.

  • Arrow functions shouldn’t be used when we need function name reference for recursion and event binding.
Summary
Arrow functions Normal functions
Binding Lexical Dynamic
this, arguments, super, new.target Don’t have these objects. Inherit from enclosing scope Have these objects.
Type of function Anonymous function expression Any type
Used as constructor?
Own prototype object?
Can this be changed? ✘(hard bound) ✔(soft bound)
Used as generators?

Note: Apart from differences between arrow functions and normal functions mentioned above, all capabilities of normal functions are available to arrow functions, including default values, destructuring, rest parameters, etc..

[1, 2, 3, undefined].map((i=0)=>i*2)
// [2, 4, 6, 0]