5

Why does the 1st map method below treat element #2 as undefined but the 2nd map method does not?

const a = [undefined,,42]; // Note the two consecutive commas.
console.log(a.map(x=>x));             // -> [undefined, undefined, 42]
console.log(a.map(x=>x===undefined)); // -> [true, undefined, false]

I expand on this with the code below. There I have some numbered examples illustrating my points. In each case I create a 3-element array and from then on I am only interested in the 2nd element. I show that this 2nd element can be considered to be undefined both by how it responds to .toString() (i.e. how it appears in console.log(elmt2)) as well as how it behaves in an equality statement (i.e. elmt2 === undefined?). Then, however, I map all the elements in the array to a constant string irrespective of what the values are (i.e. array.map(_=>'foo')), and look at the 2nd element of the mapped result. I would have expected all the outputs to be the constant string, but they aren't.

const check = (i, array) => {
  const mapped = array.map(() => 'mapped   ');
  const equalsUndefined = (array[1] === undefined);
  const mappable = (array.map(_=>1)[1] === 1);
  console.log(`${i}:  ${array[1]}  ${equalsUndefined}     ${mapped[1]}    ${mappable}`);
};

const uExplicit  = [4, undefined, 4][1];
const uEmptySlot = [4,          , 4][1];
const uNewArray  = (new Array(3)   )[1];

console.log('#:  origValue  =undef?  mappedValue  mappable?');
check(1, [4, undefined , 4]);
check(2, [4,           , 4]);
check(3, new Array(3)      );
check(4, [4, uExplicit , 4]);
check(5, [4, uEmptySlot, 4]);
check(6, [4, uNewArray , 4]);

/*
results:
#:  origValue  =undef?  mappedValue  mappable?
1:  undefined  true     mapped       true
2:  undefined  true     undefined    false
3:  undefined  true     undefined    false
4:  undefined  true     mapped       true
5:  undefined  true     mapped       true
6:  undefined  true     mapped       true
 */

Line 1 shows that an explicit undefined value can be successfully mapped in this manner.

However, lines 2 & 3 show that array elements that might be considered "empty slots" cannot be successfully mapped, even though they behave as undefined according to the two tests described above. In line 2, the value is extracted from a literal empty space between two otherwise normal array elements. In line 3, the value is extracted from an array created using new Array(arrayLength).

Bizarrely, lines 3, 4 & 5 show that the values examined in lines 1, 2 & 3 respectively can be successfully mapped if they are simply assigned to a variable first, instead of being used directly.

To be clear, the MDN documentation for arrays states that new Array(arrayLength) creates "...an array of arrayLength empty slots, not slots with actual undefined values". However, the output here shows that these values are treated as undefined, at least with respect to how they behave with .toString() and in equality statements.

So, are there two different "flavours" of undefined in JavaScript, one that can be manipulated using map and one that cannot? Is this behaviour expected/blessed (even if it's perhaps somewhat counter-intuitive)? Is there a safe way to map through an array in such a way that all undefined values can be treated identically?

UPDATE: THE SOURCE OF MY UNCERTAINTY

I have discovered why I was puzzled. It turns out that the Stack Overflow code snippet tool produces an ambiguous output when using console.log in this case which lead me to think something was undefined when in fact it was non-existent.

For the simple command console.log([undefined,,4]), the consoles in Chrome, Firefox, Safari and Opera produce correct and unambiguous output:

Chrome:

enter image description here

Firefox:

enter image description here

Safari:

enter image description here

Opera:

enter image description here

However, the output from the code snippet tool on Stack Overflow (in any browser) is ambiguous and at least misleading if not technically incorrect (depending on how you define what console.log should do). (While I eventually posted this question on Software Engineering, which does not have the code snippet tool, I originally worked through the problem on Stack Overflow, which does, leading to me seeing the confusing output shown below.) I have to split the input and output into two separate images just because they are on different parts of the screen...

Stack Overflow code snippet tool input:

enter image description here

Stack Overflow code snippet tool output:

enter image description here

Note that in this output the first and second elements are shown identically, i.e. as undefined, in contrast with all the browser console outputs above which show the first element as undefined but the element in the second position as non-existent.

Just for people's interest, note the following bug in Internet Explorer 11 when I try to show the same thing in its console:

enter image description here

  • If you want to see a new tag added, please ask on Meta so the community can decide if it's a tag that should be created. I removed the request from your question as it was irrelevant to your actual question. – RubberDuck Feb 5 '17 at 15:48
  • Using Firefox 51 I see Array [ undefined, <1 empty slot>, 42 ] and Array [ true, <1 empty slot>, false ], respectively. This seems to support the accepted answer. – Jacob Raihle Feb 6 '17 at 14:09
  • @Jacob Raihle, I agree with you. I believe that my initial question, my labelled update to my question, the accepted answer and your comment are all in agreement with each other. Do you see a conflict? – Andrew Willems Feb 6 '17 at 14:34
  • I don't see a conflict, but I must have based my comment on the old version of your question. Nevermind. – Jacob Raihle Feb 6 '17 at 15:35
6

I'd have to check the ECMAScript spec, but I suspect the situation is as follows. An array is roughly like an object with "integer" keys (plus length which I'll ignore). An "empty slot" is simply a missing key, i.e. [1,,2] is like {"0":1,"2":2}. map is then like for(var key in a){ ... }.

In the [4,undefined,4] case you have {"0":4,"1":undefined,"2":4} and so mapping x => x === undefined over it leads to

{
    "0": 4 === undefined,
    "1": undefined === undefined,
    "2": 4 === undefined
}

which is {"0":false,"1":true,"2":false} i.e. [false,true,false]. On the other hand, [4,,4] means {"0":4,"2":4} and mapping over it leads to:

{
    "0": 4 === undefined,
    "2": 4 === undefined
}

which is to say [false, undefined, false]. console.log(a) presumably just loops from 0 to a.length - 1 printing out values and {}.foo === undefined or more relevantly {"0":false,"2":false}["1"] === undefined so undefined is what is displayed.

This interpretation explains all the results that you see.

  • 1
    While not the spec, MDN (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…) is pretty definitive here. The mapping function is not called for missing elements, just as your answer describes – Dancrumb Feb 5 '17 at 14:46
  • Your approach, explaining arrays as being like objects with numeric keys, was helpful. You also provide a possible explanation for what console.log is doing, which is also helpful. Please see an update to my question which explains the source of my original confusion, and it involves exactly that, i.e. how console.log is implemented. Note that the code snippet tool on Stack Overflow does this differently than most of the browsers do in their consoles, as described in that update. – Andrew Willems Feb 5 '17 at 16:14
  • just one little question, if this analogy is right, how is 'empty' in [4,,3] store and defined? – Matian2040 Mar 6 '18 at 16:23
  • Based on the explanations in the answers, my assumption is that the 'empty' is not stored. i.e. with [4,,3], element #0 is stored as 4 and element #2 is stored as 3, and that's it. The 'empty' slot is in position for element #1 and it is not stored at all in the same way that element #14 is not stored. – Andrew Willems Mar 6 '18 at 17:33
3

This is a consequence of how map handles missing slots in an array.

[undefined, , 42].map(x=>"hello") 

yields

["hello", undefined, "hello"]

When there is missing "slots" in the input array, then the same slots will we missing in the output array.

From the spec:

callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.

In reality the array only have two entries and the mapping function is only executed twice. It is only when the array is printed as a string the "missing slots" is printed as "undefined", It would perhaps make more sense if the printing function used the consecutive-comma syntax and wrote:

["hello" , , "hello"]

But this is just a question of how arrays are printed. Using this convention your example would make more sense:

const a = [undefined,,42]; // Note the two consecutive commas.
console.log(a.map(x=>x));             // -> [undefined,,42]
console.log(a.map(x=>x===undefined)); // -> [true,,false]

The distinction is not between to kinds of undefined, rather it is between a non-existing array slot, and an existing array slot with the value undefined. Both return undefined if you ask for the value, but internally they are different things.

Arrays in JavaScript are sparse, which means a slot only "exists" if it have been explicitly assigned a value. Assigning undefined to a slot still means the slot is created. In the array literal syntax, consecutive commas means the slot is kept non-existing, while the keyword undefined causes the slot to be created and assigned the value undefined.

  • It is only when the array is printed as a string the "missing slots" is printed as "undefined". good point! – Matian2040 Mar 6 '18 at 16:33
  • Just a question tho, how is 'empty' stored in the array? please – Matian2040 Mar 6 '18 at 16:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.