The Most Underrated JavaScript Array Method
Array.prototype.includes
? The ever-headscratch-inducing .reduce
? The extra-fancy .flatMap
? JavaScript Arrays have become veritable swiss-army knives in the age of ES6 / ES9 / ES2021 / babel-thank-god-we-don’t-have-to-think-about-this, with browser support catching up faster than ever and polyfills abound when you need them. But I’d wager there’s one humble Array method that would get daily, nay hourly use by any dev wrangling indices and items under the sun, if only its true personality was understood.
That method is the unassuming Array.from
.
No prototypes here folks — Array.from
harkens straight from the Class itself. It’s cat-in-balloon-factory static.
This means you can use it without an existing array, on things that aren’t arrays at all. As such, it effectively grants you a superpower — the ability to use the entire menagerie of Array
methods on any object you want.
Note | Yes, JavaScript being JavaScript, you can actually just call Array methods on any rando object (or function, dog, cat, another method, anything, 24/7). Since JavaScript lets us grab methods directly from any object’s prototype and specify what this context should be used while that method executes, if you know the right incantations it’s a literal free-for-all. I’ve always been leery of this practice — yes you’re explicitly setting this during method execution to whatever non-Array thing, but you have absolutely zero guarantees that the method’s internals expect and operate well with whatever object you decide to give it (and that they will continue to operate well across thousands of browser versions, in perpetuity). I think it would be fair to say calls like Array.prototype.filter.call(myArrayLikeObj, (item) ⇒ { … }) are hacks that should be avoided if possible — the approach I’d like to describe in this post is an alternative that sticks firmly within the realm of standard language tools. |
Why might you want to use Array
methods on non- Array
objects? It’s not always (or often) a good idea; Array.filter
on a plain ol' object make code go boom. But it’s occasionally essential — one example happens to be extremely common when negotiating with browser APIs:
const paragraphs = document.querySelectorAll("p");
const paragraphsAboutCats = paragraphs.filter((p) => p.innerHTML.includes("cat"));
OUTPUT >>> Uncaught TypeError: paragraphs.filter is not a function
Damn the torpedoes, you might say to yourself; give me those blasted cats! And the browser would ignore you, belligerently erroring despite the fact you know you have a list of items in hand. Unfortunately, you don’t have an Array
, a sad fact that JavaScript makes very unclear — you have what’s known as a NodeList
, that just so happens to not support any of the standard methods you know and love. Enter: Array.from
.
const paragraphs = document.querySelectorAll("p");
const paragraphsAboutCats = Array.from(paragraphs)
.filter((p) => p.innerHTML.includes("cat"));
OUTPUT >>> [<p>The <code>concat()</code> method is used to merge... 🎉
Array.from
cleverly transforms the rogue NodeList
into a familiar Array
. Balance is restored, and you can move on to more interesting problems such as a feline-informed naming scheme for your conference rooms.
Before you do though, if you happen to be up on the latest ES6 trends you might have a bone to pick. Isn’t there built-in syntax to accomplish the exact same thing? That is, transform any iterable object into a bonafide JavaScript Array
?
const paragraphs = document.querySelectorAll("p");
const paragraphsAboutCats = [...paragraphs].filter((p) => p.innerHTML.includes("cat"));
OUTPUT >>> [<p>The <code>concat()</code> method is used to merge... 🎉
Indeed there is, and it’s a cool seven characters more concise. But while spread syntax may get you an in with the hottest frontend developer clique at the next standup, I’d like to illuminate a few qualities of Array.from
that might just convince you to raise it as one of your own.
Array.from is descriptive
The […paragraphs]
syntax is clear as day, said no one ever. It’s what we call an "idiom" in the biz, something that "looks weird at first, but you’ll get used to it." Don’t get me wrong, idioms and idiomatic code are great; but a requirement for obtuse syntax isn’t what makes them so — it’s their reinforcement of consistent, recognisable patterns across codebases and teams.
It’s a tricky balance — some idioms, while mysterious to newcomers, allow code to become more expressive when used well. It’s a judgement call you’ll constantly have to make; I’d personally put object spread in this category over Object.assign
; similarily I’d prefer array spread over slicing and dicing. But in the case of Array.from
, it’s a one-for-one switcheroo to use the more "descriptive" version, and array spread doesn’t save you anything apart from a couple keypresses. Let Array.from
become the new idiom for copying arrays and converting iterable objects.
Array.from is searchable
Try Googling […paragraphs]
as a budding developer fresh onto the inhospitable landscape of cross-browser industrial-grade JavaScripting, and you’ll hear the collective laughter of 1000s of search engineers at once, then silence.
Let’s try Array.from
— longer yes, but the exact same functionality, and it’s searchable:
Your interns will thank you.
Array.from can map more efficiently than Array.map! (in some cases)
Array.from
has a trick up its sleeve. It supports an optional second parameter — a so-called "map function" — which allows converting/copying an array and transforming its items in a single operation, without making any intermediate copies of the array in memory to do so. Consider this very-contrived cat-transforming JavaScript:
function capCats(catElements) {
return Array.from(catElements).map((p) => {
return p.innerHTML.replaceAll("cat", "CAT");
});
}
console.log(capCats(paragraphsAboutCats));
OUTPUT >>> [<p>The <code>conCAT()</code> method is used to merge...
As we now know, Array.from
can be used effectively on the catElements
object even though it is not an Array
; it is a NodeList
. We make this conversion so that we can invoke the Array.map
method on catElements
, a handy tool for cat-transformation (or anything else). This method does not exist on NodeList
.
However, there is a wasteful redundancy in this series of steps. Array.from(catElements)
creates an Array
copy of the catElements
list, but Array.map
also creates a copy of the array it takes as input. There’s an intermediate copy created in memory here that we don’t need — the real-world performance impacts will usually be negligible, but array data structures always have potential to become large, garbage collection behavior is ever-unpredictable. If a terser form of an idiom exists that also acts as an optimization, shouldn’t we pounce on it?
Array.from
cheekily provides just such an optimization. Our feline-finagler can be easily re-written to utilize it:
function capCats(catElements) {
return Array.from(catElements, (p) => {
return p.innerHTML.replaceAll("cat", "CAT");
});
}
Array.from
converts, copies, and maps, all in one. What’s not to love?
One Caveat
Ok, there is one tiny, insignificant, miniscule, nagging aspect not to love — the Array.from
"map function" doesn’t have the exact same signature as Array.map
's transformer function. Mozilla’s ever-excellent documentation on the subject describes the relationship well:
More clearly,
Array.from(obj, mapFn, thisArg)
has the same result asArray.from(obj).map(mapFn, thisArg)
, except that it does not create an intermediate array, andmapFn
only receives two arguments(element, index)
.
This is in contrast to Array.map
's handler, which receives three arguments — (element, index, array)
— the third is a reference to the full array being mapped over.
In practice, this enigmatic third arg is rarely used — you generally have a reference to the full array already when you map it. As always, pick the tools for the job that result in the clearest code; in cases where you need to reference the full array during map operations, Array.from(arr).map(…
sequences may end up enhancing clarity in ways that feel worthwhile. The rest of the time, don’t let this small difference dampen your enthusiasm for Array.from
— we all have to be different somehow.
Bonus round
Array.from
pairs beautifully with the semi-regular chore of generating a random hex string in JavaScript. I recently needed to conjure Sentry "trace IDs" from the void according to this spec, and Array.from
provided an opportunity to convert the following function (courtesy of the internet) into something more elegant:
function generateRandomHexString(len) {
const bytes = crypto.getRandomValues(new Uint8Array(Math.ceil(len / 2)));
const array = Array.from(bytes);
// convert bytes to strings, pad leading zeros if necessary:
const hexPairs = array.map((b) => ("00" + b.tostring(16)).slice(-2));
return hexPairs.join("").slice(0, len);
}
crypto.getRandomValues
returns a Uint16Array
here, which must be converted into a standard Array
before we .map(b ⇒ b.toString(16).padStart(2, '0'))
over it to convert bytes to string chars — without the conversion, the result will be in decimal, not hex!
Array.from
allows us to perform this conversion and mapping in a single step, without any intermediate array copies made (making the mem footprint here N*3 — the original Uint16Array
array, the standard Array
copy which we map
over, and the returned string):
function generateRandomHexString(len) {
const bytes = crypto.getRandomValues(new Uint8Array(Math.ceil(len / 2)));
// convert bytes to strings, pad leading zeros if necessary:
return Array.from(bytes, (b) => ("00" + b.tostring(16)).slice(-2))
.splice(0, len) // for odd len values, need to trim the last char
.join(""); // splice to modify array in-place and avoid copying
}
Pre-mature optimization should be avoided, but on the other hand we should celebrate tools that allow us to easily write more efficient code by default!
Sometimes we might want to provide a fallback for older browsers, where crypto
is not yet available. Array.from
comes to the rescue once again, helping us trivially write a version based on Math.random
:
function generateRandomHexString(len) {
return Array.from({ length: len }, () =>
Math.floor(Math.random() * 16).toString(16)
).join("");
}
Usage here is a bit of a hack, but allows us to construct the entire char array without any extraneous array copies whatsoever. By telling Array.from
to operate on some object with length: len
, we can convince it to execute its mapping function len
times, giving us exactly the number of chars we need. The usually-seen approach would be new Array(len).fill().map(() ⇒ …)
, but this constructs an array and immediately copies it needlessly via map
(and it’s just a lot of parentheses). I’ll take mild Array.from
hacks any day.
Note | You might be wondering what the practical difference is between these two approaches — if they both produce the same result why do we need the esoteric crypto.getRandomValues at all? The short answer is: Math.random uses an inferior Random Number Generator (RNG) which can only be considered "psuedo-random", and would never be considered "cryptographically secure," but it’s also somewhat faster (albeit less than expected, only ~3o% — see this benchmark for up-to-date results running on your browser of choice). Read into these quoted terms at your own discretion, and look here for a far more in-depth analysis. At the end of the day Math.random may be entirely sufficient for your use case — use the right tool for the job at hand. |
All together, we could construct a robust, vintage-browser-capable version of generateRandomHexString
like so:
function generateRandomHexString(len) {
if (typeof crypto === "object" && typeof crypto.getRandomValues === "function") {
var bytes = crypto.getRandomValues(new Uint8Array(Math.ceil(len / 2)));
// convert bytes to strings, pad leading zeros if necessary:
return Array.from(bytes, function (b) {
return ("00" + b.toString(16)).slice(-2);
})
.splice(0, len) // for odd len values, need to trim the last char
.join(""); // splice to modify array in-place and avoid copying
} else {
// if crypto.getRandomValues not supported, fall back to Math.random:
return Array.from({ length: len }, function () {
return Math.floor(Math.random() * 16).toString(16);
}).join("");
}
}
In closing
To those who give Array.from
the respect and mileage it deserves, I salute you.