Techniques With ES6 Default Parameter Values, Spread, and Rest

Datetime:2016-08-22 21:34:18          Topic: ECMAScript           Share

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 . 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 the 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 the 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.maxMath.minDate , 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 the  spread operator:

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

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

Forget Arguments

argumentsis 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 parameters 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 the 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 a pattern against  undefined :

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 methodheaders , 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 using the rest operator:

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

}

...And using the 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
    });   
}




About List