ponyfoo.com

ES6 Spread and Butter in Depth

Fix

Welcome to yet another installment of ES6 in Depth on Pony Foo. Previous ones covered destructuring, template literals, and most recently, arrow functions. Today we’ll cover a few more features coming in ES6. Those features are rest parameters, the spread operator, and default parameters.

We’ve already covered some of this when we talked about destructuring, which supports default values as a nod to the synergy in ES6 features I’ve mentioned yesterday. This article might end up being a tad shorter than the rest because there’s not so much to say about these rather simple features. However, and like I’ve mentioned in the first article of the ES6 in Depth series, the simplest features are usually the most useful as well. Let’s get on with it!

Rest parameters

You know how sometimes there’s a ton of arguments and you end up having to use the arguments magic variable to work with them? Consider the following method that joins any arguments passed to it as a string.

function concat () {
  return Array.prototype.slice.call(arguments).join(' ')
}
var result = concat('this', 'was', 'no', 'fun')
console.log(result)
// <- 'this was no fun'

The rest parameters syntax enables you to pull a real Array out of the function's arguments by adding a parameter name prefixed by .... Definitely simpler, the fact that it’s a real Array is also very convenient, and I for one am glad not to have to resort to arguments anymore.

function concat (...words) {
  return words.join(' ')
}
var result = concat('this', 'is', 'okay')
console.log(result)
// <- 'this is okay'

When you have more parameters in your function it works slightly different. Whenever I declare a method that has a rest parameter, I like to think of its behavior as follows.

  • Rest parameter gets all the arguments passed to the function call
  • Each time a parameter is added on the left, it’s as if its value is assigned by calling rest.shift()
  • Note that you can’t actually place parameters to the right: rest parameters can only be the last argument

It’s easier to visualize how that would behave than try to put it into words, so let’s do that. The method below computes the sum for all arguments except the first one, which is then used as a multiplier for the sum. In case you don’t recall, .shift() returns the first value in an array, and also removes it from the collection, which makes it a useful mnemonic device in my opinion.

function sum () {
  var numbers = Array.prototype.slice.call(arguments) // numbers gets all arguments
  var multiplier = numbers.shift()
  var base = numbers.shift()
  var sum = numbers.reduce((accumulator, num) => accumulator + num, base)
  return multiplier * sum
}
var total = sum(2, 6, 10, 8, 9)
console.log(total)
// <- 66

Here’s how that method would look if we were to use the rest parameter to pluck the numbers. Note how we don’t need to use arguments nor do any shifting anymore. This is great because it vastly reduces the complexity in our method – which now can focus on its functionality itself and not so much on rebalancing arguments.

function sum (multiplier, base, ...numbers) {
  var sum = numbers.reduce((accumulator, num) => accumulator + num, base)
  return multiplier * sum
}
var total = sum(2, 6, 10, 8, 9)
console.log(total)
// <- 66

Spread Operator

Typically you invoke a function by passing arguments into it.

console.log(1, 2, 3)
// <- '1 2 3'

Sometimes however you have those arguments in a list and just don’t want to access every index just for a method call – or you just can’t because the array is formed dynamically – so you use .apply. This feels kind of awkward because .apply also takes a context for this, which feels out of place when it’s not relevant and you have to reiterate the host object (or use null).

console.log.apply(console, [1, 2, 3])
// <- '1 2 3'

The spread operator can be used as a butter knife alternative over using .apply. There is no need for a context either. You just append three dots ... to the array, just like with the rest parameter.

console.log(...[1, 2, 3])
// <- '1 2 3'

As we’ll investigate more in-depth next monday, in the article about iterators in ES6, a nice perk of the spread operator is that it can be used on anything that’s an iterable. This encompasses even things like the results of document.querySelectorAll('div').

[...document.querySelectorAll('div')]
// <- [<div>, <div>, <div>]

Another nice aspect of the butter knife operator is that you can mix and match regular arguments with it, and they’ll be spread over the function call exactly how you’d expect them to. This, too, can be very very useful when you have a lot of argument rebalancing going on in your ES5 code.

console.log(1, ...[2, 3, 4], 5) // becomes `console.log(1, 2, 3, 4, 5)`
// <- '1 2 3 4 5'

Time for a real-world example. I sometimes use the method below in Express applications to allow morgan (the request logger in Express) stream its messages through winston, a general purpose multi-transport logger. I remove the trailing line breaks from the message because winston already takes care of those. I also place some metadata about the currently executing process like the host and the process pid into the arguments list, and then I .apply everything on the winston logging mechanism. If you take a close look at the code, the only line of code that’s actually doing anything is the one I’ve highlighted in yellow, the rest is just playing around with arguments.

function createWriteStream (level) {
  return {
    write: function () {
      var bits = Array.prototype.slice.call(arguments)
      var message = bits.shift().replace(/\n+$/, '') // remove trailing breaks
      bits.unshift(message)
      bits.push({ hostname: os.hostname(), pid: process.pid })
      winston[level].apply(winston, bits)
    }
  }
}
app.use(morgan(':status :method :url', {
  stream: createWriteStream('debug')
}))

We can thoroughly simplify the solution with ES6. First, we can use the rest parameter instead of relying on arguments. The rest parameter already gives us a true array, so there’s no casting involved either. We can grab the message directly as the first parameter, and we can then apply everything on winston[level] directly by combining normal arguments with the rest of the ...bits and pieces. The code below is in much better shape, as now every piece of it is actually relevant to what we’re trying to accomplish, which is call winston[level] with a few modified arguments. The piece of code we had earlier, in contrast, spent most time manipulating the arguments, and the focus quickly dissipated into a battle of wits against JavaScript itselfthe method stopped being about the code we were trying to write.

function createWriteStream (level) {
  return {
    write: function (message, ...bits) {
      winston[level](message.replace(/\n+$/, ''), ...bits, {
        hostname: os.hostname(), pid: process.pid
      })
    }
  }
}

We could further simplify the method by pulling the process metadata out, since that won’t change for the lifespan of the process. We could’ve done that in the ES5 code too, though.

var proc = { hostname: os.hostname(), pid: process.pid }
function createWriteStream (level) {
  return {
    write: function (message, ...bits) {
      winston[level](message.replace(/\n+$/, ''), ...bits, proc)
    }
  }
}

Another thing we could do to shorten that piece of code might be to use an arrow function. In this case however, it would only complicate matters. You’d have to shorten message to msg so that it fits in a single line, and the call to winston[level] with the rest and spread operators in there makes it an incredibly complicated sight to anyone who hasn’t spent the last 15 minutes thinking about the method – be it a team mate or yourself the week after you wrote this function.

var proc = { hostname: os.hostname(), pid: process.pid }
function createWriteStream (level) {
  return {
    write: (msg, ...bits) => winston[level](msg.replace(/\n+$/, ''), ...bits, proc)
  }
}

It would be wiser to just keep our earlier version. While it’s quite self-evident in this case that an arrow function only piles onto the complexity, in other cases it might not be so. It’s up to you to decide, and you need to be able to distinguish between using ES6 features because they genuinely improve your codebase and its maintainability, or whether you’re actually decreasing maintainability by translating things into ES6 just for the sake of doing so.

Some other useful uses are detailed below. You can obviously use the spread operator when creating a new array, but you can also use while destructuring, in which case it works sort of like ...rest did, and a use case that’s not going to come up often but is still worth mentioning is that you can use spread to pseudo-.apply when using the new operator as well.

Use Case ES5 ES6
Concatenation [1, 2].concat(more) [1, 2, ...more]
Push onto list list.push.apply(list, [3, 4]) list.push(...[3, 4])
Destructuring a = list[0], rest = list.slice(1) [a, ...rest] = list
new + apply new (Date.bind.apply(Date, [null,2015,31,8])) new Date(...[2015,31,8])

Default Operator

The default operator is something we’ve covered in the destructuring article, but only tangentially. Just like you can use default values during destructuring, you can define a default value for any parameter in a function, as shown below.

function sum (left=1, right=2) {
  return left + right
}
console.log(sum())
// <- 3
console.log(sum(2))
// <- 4
console.log(sum(1, 0))
// <- 1

Consider the code that initializes options in dragula.

function dragula (options) {
  var o = options || {};
  if (o.moves === void 0) { o.moves = always; }
  if (o.accepts === void 0) { o.accepts = always; }
  if (o.invalid === void 0) { o.invalid = invalidTarget; }
  if (o.containers === void 0) { o.containers = initialContainers || []; }
  if (o.isContainer === void 0) { o.isContainer = never; }
  if (o.copy === void 0) { o.copy = false; }
  if (o.revertOnSpill === void 0) { o.revertOnSpill = false; }
  if (o.removeOnSpill === void 0) { o.removeOnSpill = false; }
  if (o.direction === void 0) { o.direction = 'vertical'; }
  if (o.mirrorContainer === void 0) { o.mirrorContainer = body; }
}

Do you think it would be useful to switch to default parameters under ES6 syntax? How would you do that?

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (15)

Humberto wrote

First of all, many thanks for this article. It always good to have a nice and detailed explanation of all the new features of ES6. Very well written.

About the last part, I would change this line:

var o = options || {};

and put it in the function definition

function dragula (o = {}) {

Would it be it?

Alex Hutsulyak wrote

@Author - Thanks for a great series of articles :) Following those with a great effort :)

@Humberto, if I’ve got it right, here is a right answer:

function dragula ({ moves = always, 
                            accepts = always, 
                            invalid = invalidTarget, 
                            containers = initialContainers || [], 
                            isContainer = never, 
                            copy = false, 
                            revertOnSpill = false, 
                            removeOnSpill = false, 
                            direction = ‘vertical’, 
                            mirrorContainer = body } = {}) {
}

Not sure about ‘OR’ operator though, as didn’t tested this code

P.s.: sorry for double post. For some reason for first time, comment editor didn’t had any text format controls, only inputs for name/email/optional and actual textarea.

Humberto Gómez wrote

@Alex I don’t think your proposal will work since you’re trying to assign an object as a parameter name:

function dragula (paramName = value){}

Where paramName is the name of the parameter and value is the default value. Your code tries to assign an object as a parameter name and also the syntax inside the object itself is also incorrect:

This is what happens when trying to run it:

This is what happens when trying to run it
This is what happens when trying to run it

P.D. @Alex, thanks for replying :D love to discuss this kind of topics with other coders.

Alex Hutsulyak wrote

Actually I don’t get, why this code doesn’t work for you. I’ve got exact same error, when tried to run this ES6 snippet in ES5 environment. But after transpiling this one to ES5, it works. Only problem with this snippet, is that original ‘dragula’ function is changing options object by it’s reference, as I see from original code provided by author of article, and snippet only assigns default values for provided parameter object’s properties. Full implementation could look like:

function dragula ({ moves = always, 
                accepts = always, 
                invalid = invalidTarget, 
                containers = initialContainers || [], 
                isContainer = never, 
                copy = false, 
                revertOnSpill = false, 
                removeOnSpill = false, 
                direction = 'vertical', 
                mirrorContainer = body } = {}) {


 return {
    moves: moves,
    accepts: accepts,
    invalid: invalid,
    containers: containers,
    isContainer: isContainer,
    copy: copy,
    revertOnSpill: revertOnSpill,
    removeOnSpill: removeOnSpill,
    direction: direction,
    mirrorContainer: mirrorContainer
  }
}

So as a result, we’ve got a function, which receives object as an argument, and returns object with initialized default properties.

That’s actually what I meant :)

Btw, here is a link to working example https://goo.gl/vGbX6u (Babel.io’s playground)

Nicolas Bevacqua wrote

Alex, you can do one better! (solution) Check out the article on destructuring.

Alex Hutsulyak wrote

Btw, in my personal opinion, provided by author ‘dragula’ function won’t work, as it doesn’t return anything. That means, that if we won’t pass anything to this function, actually nothing will happen, because:

var o = options || {} //in case options were passed - pass options reference to 'o' variable, if not - create new empty object

as a result of this, we will assign default values for this newly created empty object, and then will simply loose it right after function execution will come to an end.

Alex Hutsulyak wrote

Oh, yeah. Totally forgot about this one :) Thanks Nicolas

CrocoDillon wrote

What about this solution using return { ...defaultOptions, ...options }? Easier to read and cleaner ES5 output.

Dylan wrote

It’s lame that es6 doesn’t support left hand splats, coffee script supports it.

(a, b…, c)->

Makes sense sometimes, if not just for completeness.

Alex Hutsulyak wrote

@Humberto, if I’ve got it right, here is a right answer:

function dragula ({ moves = always, accepts = always, invalid = invalidTarget, containers = initialContainers || [], isContainer = never, copy = false, revertOnSpill = false, removeOnSpill = false, direction = ‘vertical’, mirrorContainer = body } = {}) { }

Not sure about ‘OR’ operator though, as didn’t tested this code

Karl P. wrote
function sum () {
  var numbers = Array.prototype.slice.call(arguments) // numbers gets all arguments
  var multiplier = numbers.shift()
  var base = numbers.shift()
  var sum = numbers.reduce((accumulator, num) => accumulator + num, base)
  return multiplier * sum
}

The arrow function returns accumulator + num, base. The base variable doesn’t make sense to me here. Am I missing something?

Nicolas Bevacqua wrote

base initializes accumulator. Go google Array.prototype.reduce!

CrocoDillon wrote

You can also initialize the accumulator with 0, that way you don’t need to shift “base” out. Makes it more consistent and readable too.

bfavaretto wrote

Your Date example in the ES5/ES6 table seem to be wrong. Shouldn’t it be new Date(…[2015,7, 31]) and new (Date.bind.apply(Date, [null, 2015, 7, 31]))? Note the order of the arguments.

Danny Shaw wrote

Nice read. This is my standard pattern…

function dragula (options) {
  var o = {
    moves: always,
    accepts: always,
    invalid: invalidTarget,
    containers: initialContainers,
    isContainer: never,
    copy: false,
    revertOnSpill: false,
    removeOnSpill: false,
    direction: 'vertical',
    mirrorContainer: body,
    ...options
  };