Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Array.prototype.map ignore empty slots in a sparse array whereas Array.prototype.join does not?

The Array.prototype.map function works as expected when applied on an array with undefined values:

const array = [undefined, undefined, undefined];
console.log(array.map(x => 'x')); // prints ["x", "x", "x"]

However, when using map on a sparse array with empty slots, it does not map them to 'x' as in the previous example. Instead, it returns undefined values:

const array = [,,,];
console.log(array.map(x => 'x')); // prints [undefined, undefined, undefined]

Even if we have an array with a mix of empty slots and actual values, only the latter ones are mapped:

const array = [,'a',,'b',];
console.log(array.map(x => 'x')); // prints [undefined, "x", undefined, "x"]

In contrast, I noticed Array.prototype.join works on empty slots:

const array = [,,,,];
console.log(array.join('x')); // prints "xxx"

Why does join treat empty slots as valid elements, but map does not?

Furthermore, in the join documentation, they mention that if an element is undefined, null or an empty array [], it is converted to an empty string. They do not mention empty slots, but it seems they are also converting them to an empty string.

Is it then a problem in the MDN documentation? And why not having join also ignore empty slots in the same way map does? It seems to be either a problem in the documentation or in the implementation of join.

like image 947
Alberto Trindade Tavares Avatar asked Dec 23 '22 19:12

Alberto Trindade Tavares


2 Answers

join attempts to produce a serialized representation of the array. map produces a projection of the elements of an array through some transforming function.

With map, it is possible to say: "As you step through the array, if you encounter an index that has no property, leave that property similarly unset in the output array." For all existing properties, output indices will still correspond to their input indices, and the missing properties are skipped in both the input and output.

With join's string output, we can't really do this. If we join [,'a',,'b',], an output of ,a,,b, is the best way to represent this. An output that skips missing properties -- i.e., a,b -- would be hugely misleading, appearing to be a length-2 array with elements at indices 0 and 1.

Unlike map, which can produce an array with variously present or absent properties, join is stuck rendering a string output, which cannot readily distinguish missing vs. empty properties in its output without hugely misleading results.

For completeness, here are the actual ECMAScript-specified behaviors where the function loops through the input array (in each, k is the loop variable):

Array.prototype.join

Repeat, while k < len

  • If k > 0, set R to the string-concatenation of R and sep.
  • Let element be ? Get(O, ! ToString(k)).
  • If element is undefined or null, let next be the empty String; otherwise, let next be ? ToString(element).
  • Set R to the string-concatenation of R and next.
  • Increase k by 1.

Array.prototype.map

Repeat, while k < len

  • Let Pk be ! ToString(k).
  • Let kPresent be ? HasProperty(O, Pk).
  • If kPresent is true, then
    • Let kValue be ? Get(O, Pk).
    • Let mappedValue be ? Call(callbackfn, T, « kValue, k, O »).
    • Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
  • Increase k by 1.

Even if you don't know how to read all of this, it's plain to see that map includes a HasProperty check in the second loop step. join explicitly says "If element is undefined or null, let next be the empty String." Get(O, ! ToString(k)) is a usual property lookup which, for ordinary objects, yields undefined when a property is absent, so the "If element is undefined" case applies.

It's worth noting that the MDN documentation simplifies its information in order to focus on the most common cases instead of adhering to rigorous completeness. (I would say that sparse arrays are an uncommon case.) In particular, they say that an empty array will serialize to the empty string, which is true. This is true in general for any value that has a toString function which returns an empty string:

["foo", { toString: a=>""}, "bar"].join()

This will produce the output foo,,bar.

like image 154
apsillers Avatar answered Dec 28 '22 22:12

apsillers


If you're here NOT for excerpts from tutorials, but for practical ways to get array methods behave in desired way, consider the following:


const array = [,,,];
console.log([...array].map(x => 'x'));

...if you need resulting array of initial size, or

const array = [,'a',,'b',]
console.log([...array].filter(Boolean).map(x => x+'x'));

...if you need to skip empty slots

like image 31
Yevgen Gorbunkov Avatar answered Dec 29 '22 00:12

Yevgen Gorbunkov