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
    });   
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s