Learning ES6: Rest & Spread Operators

Let’s continue the Learning ES6 series and look at the new rest and spread operators introduced in ECMAScript 6 and how they can make our code both cleaner and clearer. We’ve already talked about default parameters and block-scoping, so play catch-up if you need to!

TL;DR

The ES6 rest operator is three dots (...) preceding a function header parameter and should be able to completely replace the need for the problematic arguments special variable:

function join(separator, ...values) {
	return values.join(separator);
}

// all of the parameters after the first
// are gathered together into `values`
// which is a true `Array`
// output: "one//two//three"
console.log(join('//', 'one', 'two', 'three'));

And we should no longer need the apply function with the new ES6 spread operator, three dots (...) preceding a function call argument:

function volume(width, length, height) {
	return width * length * height;
};

// the array values are separated into
// separate parameters
// output: 80 (2 * 8 * 5)
console.log(volume(...[2, 8, 5]));

These quick examples are only scratching the surface of how to use the rest and spread operators. Be sure to check out the full suite of parameter handling code examples (a part of the Learning ES6 Github repo) and keep reading.

Rest operator

We learned previously that default parameters handle the case where a caller passes less parameters than what a function declares. JavaScript also allows for functions to be called with more parameters than the function declares. That’s where the rest operator comes in.

A common use-case where a function will have less parameters declared is when the function can take an arbitrary number of parameters. Let’s say we wanted to write a join function similar to the Array.prototype.join() except we want to specify individual parameters instead of an array.

With ES5 we could implement it using the special arguments variable like so:

function join(separator) {
	var values = [];

	for (var argNo = 1; argNo < arguments.length; argNo++) {
		values.push(arguments[argNo]);
	}

	return values.join(separator);
};

// output: "one++two++three"
console.log(join('++', 'one', 'two', 'three'));

The arguments special variable is problematic for many reasons; one being that it’s not an actual Array object, so methods like slice are unavailable to use. Also because we have the separator parameter, we have to start at index 1 of arguments, which is pretty annoying. Lastly, just looking at our join function, it’s not immediately discoverable that it actually takes more than one parameter, let alone that it supports an infinite number of them.

ES6 introduces the rest operator, three dots (...) that precede a named parameter. That parameter is now a rest parameter that is an Array containing the rest of the parameters (hence the name!).

Here’s join rewritten using ES6 rest parameters:

function join(separator, ...values) {
	return values.join(separator);
}

// all of the parameters after the first
// are gathered together into `values`
// which is a true `Array`
// output: "one//two//three"
console.log(join('//', 'one', 'two', 'three'));

In the example, values is an Array of all of the parameters passed after '//' making it much easier to use. Also, it’s much clearer looking at the join function that it does take an additional unlimited set of parameters.

One rest parameter per function

One caveat with rest parameters is that unlike default parameters there can only be one per function and it must be the last parameter declared in the function header. Attempting to have multiple rest parameters or putting one before other parameters will throw a SyntaxError:

function afterRest(first, ...second, third) {
	// SyntaxError: parameter after rest parameter
}
function multipleRest(first, ...second, ...third) {
	// SyntaxError: parameter after rest parameter
}

If you’re using a transpiler, you will get an error when trying to transpile your ES6 code down to ES5.

Enforcing maximum arity

ES6 unfortunately doesn’t provide a mechanism for enforcing a maximum arity (number of passed parameters) of a function. However, you can leverage rest parameters to hack around the lack of support.

function max(...values) {
	// only want as many a 3 parameters
	// so throw error if over
	if (values.length > 3)
		throw Error('max 3 parameters allowed!');

	// use destructuring to get values
	// into variables
	let [a, b, c] = values;

	return Math.max(a, b, c);
}

// not an error
// returns 3
max(1, 2, 3);

// error!
max(1, 2, 3, 4);

The problem with this approach is that the function actually wants to define a, b and c as its parameters, but because it needs to do arity validation, those variables are instead assigned in the function body using destructuring (which we’ll talk about in our next post).

We could clean things up a little bit:

function max(a, b, c, ...shouldBeEmpty) {
	if (shouldBeEmpty.length > 0)
		throw Error('max 3 parameters allowed!');

	return Math.max(a, b, c);
};

// not an error
// output 6
max(4, 5, 6);

// error!
max(4, 5, 6, 7);

This is a little better, but introduces a 4th parameter, shouldBeEmpty, that’s not intended to be a part of the actual code, which could be confusing.

This may be the one (and only) case where using arguments may be preferable over a rest parameter. In pretty much every other case, rest parameters should replace uses of arguments.

Spread operator

While rest parameters use the rest operator to combine zero or more parameters into a single array parameter, the spread operator does just the opposite. It separates an array into zero or more parameters.

But before we get into how the spread operator works, lets first take a look at the ES5 code it is intending to replace.

function merge() {
	var masterObj = {};

	// iterate over `arguments` merging each
	// into `masterObj` to generate flattened
	// object
	for (var i = 0; i < arguments.length; i++) {
		var obj = arguments[i];;
		for (var key in obj)
			masterObj[key] = obj[key];
	}

	return masterObj;
}

let merged = merge(
	{
		count: 5,
		delay: 2000,
		early: true,
		message: 'Hello'
	},
	{
		early: false
	}
);

// output:
// {count:5, delay:2000, early:false, message:'Hello'}
console.log(merged);

The merge function is designed to take an arbitrary number of objects and flatten them into one object. Calling merge is easy when you have individual objects, but what happens when you want to flatten array of objects? You have to use apply:

var objectsList = [
	{
		count: 5,
		delay: 2000,
		early: true,
		message: 'Hello'
	},
	{
		early: false
	}
];
var merged = merge.apply(undefined, objectsList);

// output:
// {count:5, delay:2000, early:false, message:'Hello'}
console.log(merged);

This works okay. For those of us JavaScript ninjas this sort of thing is old hat. But the code is a bit weird, especially the fact that we have to pass undefined as the first (context) parameter. And then there’s always the confusion between apply and call. The former takes an array, while the latter takes an unbounded list of parameters.

Now instead of apply, we can use the spread operator (along with a rest parameter!):

function merge(...objects) {
	let masterObj = {};

	// iterate over `objects` merging each
	// into `masterObj` to generate flattened
	// object
	for (let i = 0; i < objects.length; i++) {
		let obj = objects[i];;
		for (let key in obj)
			masterObj[key] = obj[key];
	}

	return masterObj;
}

let objectsList = [
	{
		count: 5,
		delay: 2000,
		early: true,
		message: 'Hello'
	},
	{
		early: false
	}
];
let merged = merge(...objectsList);

// output:
// {count:5, delay:2000, early:false, message:'Hello'}
console.log(merged);

The spread operator looks exactly like the rest operator. It is the same three dots (...). The only difference is that it is used in function calls and array literals instead of function parameter declarations. The spread operator should be able to replace the majority, if not all, uses of apply.

At first, using the spread operator may not seem like much of an improvement over apply, besides no longer having to specify undefined. However, the spread operator can be used anywhere in a function call and may be used more than once as well. Take a look at this example:

let merged = merge(
	{count: 10},
	...objectsList,
	{delay: 1500}
);

// output:
// {count:5, delay:1500, early:false, message:'Hello'}
console.log(merged);

Now we’re specifying individual objects as well as the array. If we were going to still try to use apply we would first have to build a new array including the individual objects. Spread operator to the rescue!

JavaScript engine support

Good news! According to the ECMAScript 6 Compatibility table, all the major JavaScript engines (browsers, servers & transpilers) support the rest and spread operators.

Additional Resources

As always, you can check out the Learning ES6 examples page for the Learning ES6 Github repo where you will find all of the code used in this article running natively in the browser (for those that support rest & spread operators).

You can also practice everything you’ve learned on ES6 Katas. It uses a TDD (test-driven development) approach for you to implement ES6 features such that all of the tests pass. I highly recommend it!

Finally, if this information wasn’t enough, there is even more you can read concerning rest & spread operators in ES6:

Coming up next…

We will be continuing the Learning ES6 series next week by looking at how ES6 destructuring can help us write succinct code through shorthand assignments. Until then…

FYI

This Learning ES6 series is actually a cross-posting of a series with the same name on my personal blog, benmvp.com. The content is pretty much the exact same except that this series will have additional information on how we are specifically leveraging ES6 here in Eventbrite Engineering. I’ll also be tackling the features in a different order than I did in my personal blog. The original rest and spread operator blog post was part of a larger parameter handling post.

2 thoughts on “Learning ES6: Rest & Spread Operators

  1. you don’t even need the merge function anyonre with spread operators in objects.
    let obj1 = { a:1, b:2 }
    let obj2 = { b:3, c:4}
    let obj3 = { …obj1, …obj2} //will result in {a:1, b:3, c:4}

    notice how properties with the same keys get overridden so the last one wins 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *