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).

or , 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).

, then 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()