Never Use Array.from() to Convert Strings to Arrays
TL;DR: Splitting a string into an array is about 70 times faster with 'a string'.split('')
than Array.from('a string')
.
In my interview prep I came across an article on three ways of reversing a string in JavaScript by Sonya Moisset. The problem of reversing a string may seem trivial (and it is), but I'm never "too good" to come up with an answer for a problem. The author of the article showed three perfectly acceptable ways of reversing a string, but I'll share a fourth way that I like. I've been on a .reduce
binge lately, and I've been thinking of ways to shoehorn it into every iterator I write, so I figure I'll do it again here.
The normal way
If you were a sane person confronted with the problem of reversing a string, your first instinct might be to avoid reinventing the wheel and just use the built in Array.prototype.reverse
method. And to do that you would have to turn your string into an array so you would probably write something like this:
String.prototype.reverseNormal = function(){
return this.split('').reverse().join('');
};
Easy and readable. If you wrote this on the whiteboard during an interview you'd probably get the nod. But what if you wanted a confused look instead?
The .reduce() way
String.prototype.reverseReduce = function(){ return Array.from(this).reduceRight( (prev, current) => { return prev + current; }); };
This algorithm uses Array.from
to convert the string into an array such that each element is a character in the string. It then calls .reduceRight
which functions similarly to regular old .reduce
except it iterates from right to left. Since we're not passing a second argument into .reduceRight
to define a starting point, it will set prev
, which is our accumulator, to the last element by default. And current
, which is the current element at each call, will begin as the second to last element. At each call we build a reversed string through return prev + current
by the nature of working backwards from the end of the string. Since we iterate through the array once linearly, the time complexity is O(n).
Benchmarks
I wont bury the lede: the reduce way is significantly slower. Benchmarks were run on a Cloud9 Free Workspace using Benchmark.js in Node v7.9.0. Each method was tested twice: once with a long string ('abcdefg'.repeat(99)
) and once with a short string ('abcdefg'
). Here are the results:
String#reverseNormal Long x 39,643 ops/sec ±4.08% (73 runs sampled)
String#reverseNormal Short x 845,394 ops/sec ±3.59% (69 runs sampled)
String#reverseReduce Long x 5,955 ops/sec ±3.70% (75 runs sampled)
String#reverseReduce Short x 175,195 ops/sec ±4.40% (68 runs sampled)
Fastest is String#reverseNormal Short
The results are fairly clear: reversing a string the normal way is faster. The bottleneck, however, seems to be in the Array.from
method. I set up a test suite in which the only action was splitting a string. Two tests performed it with Array.from(someString)
and two tests performed it with someString.split('')
. The tested strings were the same as the ones used in the previous benchmarks.
Array#from Long x 10,322 ops/sec ±1.88% (75 runs sampled)
Array#from Short x 326,023 ops/sec ±2.11% (76 runs sampled)
String#split Long x 762,177 ops/sec ±3.33% (83 runs sampled)
String#split Short x 6,611,921 ops/sec ±2.53% (76 runs sampled)
String.prototype.split
is almost 74 times faster than Array.from
for converting long strings to arrays. Interestingly, if we substitute in this.split
into the String.reverseReduce
method instead of Array.from(this)
, the results get a lot closer:
String#reverseNormal Long x 27,795 ops/sec ±3.43% (69 runs sampled)
String#reverseNormal Short x 814,784 ops/sec ±5.00% (67 runs sampled)
String#reverseReduce Long x 36,910 ops/sec ±3.65% (75 runs sampled)
String#reverseReduce short x 1,061,802 ops/sec ±4.60% (68 runs sampled)
The two algorithms are almost equally performant when we do it this way. So the final .reverseReduce
method looks like this:
String.prototype.reverseReduce = function(){
return this.split('').reduceRight( (prev, current) => {
return prev + current;
});
};
Conclusion
Never use Array.from()
to convert a string of any length to an array. Instead, prefer String.prototype.split
. The 2016 ECMAScript spec for String.prototype.split and 2016 ECMAScript spec for Array.from are available here. Someone with a deeper understanding of JavaScript can probably do a much better job than I at explaining the performance disparity.