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
.
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
.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With