ES2015 Array.from, rest, and spread

Kerri Shotts
8 min readJul 21, 2017

--

Everything we’re going to cover, all in one picture!

Some ES2015 features are pretty easy to grasp and you can be productive with them pretty quickly. I wanted to point out a few of those features in this post. We’ll cover Array.from, and those three dots* that seem to pop up just about everywhere in modern JavaScript code.

* Otherwise known as an ellipsis: ...

Also: Playground for the image

Array.from

Note: Examples are in this playground.

How many times have you written code that looks like this?

var elNodeList = document.querySelectorAll("p"),
els = [].slice.call(elNodeList);

or

var args = [].slice.call(arguments);

Sigh… if I only had a nickel…

Anyway, with Array.from() we can bypass slice (and if you’re like me, the inevitable Google search to remember if it wasn’t splice instead) and use the following:

const els = Array.from(document.querySelectorAll("p"));const args = Array.from(arguments);

Done, and done!

Well, not quite. As is obvious from the above code, Array.from takes something that quacks like an array and converts it into something that is a real array, but because of this fact, it is capable of quite a lot of things!

For example, Array.from can be used to split a string into its component characters:

const chars = Array.from("Hello!");
// chars = ["H", "e", "l", "l", "o", "!"]

Or, we could do something like this:

const items = Array.from({
"0": "a",
"1": "b",
"2": "c",
"length": 3
});
// items = ["a", "b", "c"]

But that’s not all — Array.from doesn’t just take one argument — it can take three. Here’s the signature:

Array.from(arrayLike [, mapFn [, thisArg]]) -> Array

Hmm — interestingly enough, Array.from acts a bit like Array.map, and is mostly functionally equivalent to Array.from(arrayLike).map(mapFn [, thisArg]) so why would we ever do anything else? Simple: it saves some memory and also avoids some unexpected results by avoiding the creation of an intermediate array. This isn’t something you’ll run into with run-of-the-mill array-likes, but can cause problems with certain array subclasses, like TypedArrays.

Side note: TypedArray also has a from method, which converts the array-like into a typed array. It has some subtle differences, though, so be sure to check out Mozilla’s documentation.

So we could take the above and write the following:

const items = Array.from({
"0": "a",
"1": "b",
"2": "c",
"length": 3
}, c => c.toUpperCase());
// items = ["A", "B", "C"];

We can also use this feature to create arrays of arbitrary length. An array-like only needs to contain a length property, so we can do this:

const tenIntegers = Array.from({length:10}, (_, idx) => idx);// tenIntengers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

We can use this to create arbitrary sequences too, which can be quite useful. For example, we can easily create the first few powers of two:

const firstPowersOfTwo = Array.from({length: 11}, (_, idx) => 
2 ** idx);
// [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]

It’s also worth mentioning that Array.from works on anything that is Iterable, so it works on Maps, Sets, and Generators as well. Those are larger topics in and of themselves, so look for those articles in the future!

Rest

The “rest” operator (...) collects the remaining items from an iterable into an array. It’s used hand-in-hand with destructuring and with argument lists, and is really pretty awesome. You can see the full examples in the playground.

Note: In this section I’m only going to cover rest’s association with iterables. There is a proposal at stage 3 that allows it to work with objects, and you’ve probably already seen it in production code when using Babel as a transpiler alongside React or other similar frameworks. Even so, it’s not standard yet, so I’m not covering it in this post.

Let’s imagine that we’re writing a logging function that needs to take a format string and an arbitrary number of additional arguments. In ES5 we’d have written the following:

function log(formatStr) {
var args = [].slice.call(arguments, 1);
/* do something with formatStr and args */
}

I have several problems with a function like this:

  • We have to convert arguments to an array using [].slice.call.
  • We have to remember to avoid the first argument, since we don’t want to to treat our format string as an additional argument.
  • It’s not at all self-documenting — the arity (number of arguments taken) looks like 1, but in reality, the function can take any number of arguments. Without additional documentation, it’s not immediately obvious that this function can take any number of arguments, and is apt to confuse any IDE that attempts to provide some degree of code insight.

But with ES2015, things become clearer:

function log(formatStr = "", ...args) {
/* do something with formatStr and args */
}

So, what do I like about this? Several things:

  • args is an actual array containing all the arguments after the format string — no fiddling with slice and no need to specify an offset.
  • Self-documenting. We know that if nothing is provided, formatStr will be an empty string, and it’s obvious from the signature that the function can accept any number of additional arguments.
  • Extra bonus: args is always guaranteed to be an Array. If no additional arguments are passed, it just happens to be an empty array with length zero.

The rest operator works similarly with destructuring as well:

const [favoriteColor, ...otherColorsILike] = "purple blue pink red black".split(/\s+/);

Here, favoriteColor will receive the value “purple”, and otherColorsILike will be an array of ["blue", "pink", "red", "black"] .

There is a catch to the operator, and it’s implied in the name: it can only collect the rest of the items in a list. That is, it has to be at the end of a list — it can’t be in the middle somewhere. Which… is kind of unfortunate. I mean, how cool would be to write something like this:

const [favoriteColor, ...otherColorsILike, leastFavoriteColor] = "purple blue pink red black".split(/\s+/);

But don’t do it — it won’t work. If you don’t believe me, try it. You’ll get an error that reads like “Unexpected token ‘,’. Expected a closing ‘]’ following a rest element destructuring pattern.”

Spread

The “spread” operator (...) does essentially what it says on the tin — it spreads something iterable into a list. This comes in handy in a few different scenarios.

But wait… the “spread” operator uses the same token as the “rest” operator Yes, yes it does. Fun, right? That said, which one you’re using is obvious from context. You’ll only use the “rest” operator to collect remaining items into an array, and you’ll only be using the “spread” operator to spread an array into a list.

So where does this come in handy? Well, in a lot of places, it turns out! Be sure to check out the corresponding playground.

Function Calling

Remember our old friend Function.prototype.apply()? If you don’t, apply would let us call a function, passing an array posing as the arguments to that function. It also required us to pass a thisArg as the first argument, so when using it, we always had to make a choice about what this should be. Here’s what it looked like in ES5:

someFunction.apply(undefined, argArray);
// equivalent to someFunction(argArray[0], argArray[1], ...)
// or for methods:
obj.someMethod.apply(obj, argArray);
// equivalent to obj.someMethod(argArray[0], argArray[1], ...)

In ES2015, we can do this instead:

someFunction(...argArray);// for methods:
obj.someFunction(...argArray);

I don’t know about you, but I find the ES2015 version a lot easier to read.

Aside: What’s the value of this in the ES2015 example? Well, it gets complicated! If someFunction is an arrow function, this will be inherited from the function’s surrounding lexical scope. Otherwise it will be determined by the engine’s mode (strict or loose). If we called it like a method, though, this would be the object instance (unless it was an arrow function). Yeah… this still manages to be confusing, even in ES2015.

It’s also worth noting that you can spread at any point in the argument list:

someFunction(1, 2, ...argArray, 4);
// --> someFunction(1, 2, argArray[0], argArray[1], ..., 4);

Array Concatenation

Imagine we have two arrays and we want to concatenate them into a single array. In ES5 we’d write this:

var a = [1, 2, 3],
b = [4, 5, 6],
aAndB = a.concat(b);
// aAndB = [1, 2, 3, 4, 5, 6]

The spread operator gives us another way of doing this:

const a = [1, 2, 3],
b = [4, 5, 6],
aAndB = [...a, ...b];

I’ll leave it to you as to which is clearer in this case. Ultimately, though, the spread operator provides quite a bit of flexibility when it comes to combining arrays.

Array Copy

How would you (shallow) clone an array in ES5? You might do this:

var a = [1, 2, 3],
b = a.map(function(i) { return i; });

But that’s actually overkill. You can also do it like this:

var a = [1, 2, 3],
b = a.slice();

In ES2015, though, it’s even easier (and, to my eye, more obvious):

const a = [1, 2, 3],
b = [...a];

As with the ES5 examples, though, this is just a shallow clone. You’d have to do a bit more work to create a deep clone.

In place of Array.from

Now, it might have been easy to miss, but I did mention that this works on iterables. So we could technically do this instead of using Array.from:

let els = [...document.querySelectorAll("p")];

Which one you prefer is largely up to you and your team.

Note: This works only for iterables, so quacking like an array isn’t completely sufficient. See the sources for this post for ways to convert an array-like into an iterable.

Performance

There’s a lot of cool things you can do with these three ES2015 features. However, like all things ES2015, I do want to consider performance a bit. Again, keep in mind that it’s still early days for ES2015 engines and optimization, so performance should continue to improve as time goes on.

That said, on this topic, the corresponding ES5 methods are generally faster than the ES2015 counterparts. Sometimes by quite a bit, even, depending on the engine. http://incaseofstairs.com/six-speed/ has a good comparison of the performance difference when using spread and rest, and it turns out that while using the rest operator is often faster, the spread operator is often quite a bit slower.

Again, would I let the performance stop me from using these features in production code? Absolutely not —microbenchmarks don’t necessarily reflect the real world, and there are all sorts of edge cases, fast paths, and the like that can change the results. Never mind that performance can vary quite a bit between JavaScript engines.

Like what I’m doing? Consider becoming a patron!

Creating articles, technical documentation, and contributing to open source software and the like takes a lot of time and effort! If you want to support my efforts would you consider becoming my patron on Patreon?

--

--

Kerri Shotts

JavaScript fangirl, Technical writer, Mobile app developer, Musician, Photographer, Transwoman (she/her), Atheist, Humanist. All opinions are my own. Hi there!