Why array with spread and map generate an array with undefined values - javascript

Could someone explain me why this statement generates a new array of null values instead a array of 1's?
let a = [...Array(3).map(_ => 1)];
console.log(a);
results [null, null, null] instead of [1,1,1]
But, the following code...
let b = [...Array(3)].map(_ => 1)
console.log(b);
results [1,1,1]

Some might consider this a dup of the post I referred to in the comments. The gist is that the array constructor creates an array of undefined references, and the spread operator creates an array of references to undefined.
map skips undefined references, but maps happily over refs to undefined. See it with a console.log() side effect.
new Array(3).map(el => console.log('this code is never reached'))
Compared this to
[...new Array(3)].map(el => console.log(el))

When you creating empty array that have a length, all slots are unassigned, they don't even have undefined value. Map on
the other hand, can only iterate thru assigned values.
Good explanation is on developer.mozilla.org
MDN Array()->Parameters->arrayLength
(...) Note: this implies an array of arrayLength empty slots, not slots with actual undefined values (...)
MDN map()->description
(...) callback is invoked only for indexes of the array which have assigned values (...)
This is why on Array(3).map(_=>1), map() callback have no effect.
Second part is how spread is working. When you are spreading array with 3 empty slots, you are creating new array, that have
filled slots. Now you have all slots with value undefined. From now, map will work as expected.
Examples:
Welcome to Node.js v12.20.1.
Type ".help" for more information.
> Array(3)
[ <3 empty items> ]
> [...Array(3)]
[ undefined, undefined, undefined ]
> const a = []
> a.length = 2
> a
[ <2 empty items> ]
> [...a]
[ undefined, undefined ]

Related

Object.keys() vs Array.prototype.keys()

let arr = new Array(3)
console.log(Object.keys(arr)) // []
console.log([...arr.keys()]) // [0,1,2]
Why is there a difference? It seems that an array in javascript can simultaneously have a length of 3 and be empty. How do you know the behavior of the different functions that operate on sparse lists?
Object.keys() does expect the parameter to be of type object and will cast anything else implicitly to one. Passing an empty array will be casted to an empty object I guess: Object.assign({}, new Array(3)) will give {}

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.
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
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.
This is how JS handles sparse arrays. Consider:
> let a = [,,,,,]
> a
[ <5 empty items> ]
> let b = [undefined,undefined,undefined,undefined,undefined]
> b
[ undefined, undefined, undefined, undefined, undefined ]
> let c = [,,'x',,]
> c
[ <2 empty items>, 'x', <1 empty item> ]
Empty items do not actually take up any space (beyond some overhead).

When can an empty array be iterated over?

Defining arrays in javascript using Array(n) defines an empty array, please correct me if I am wrong. Now if I write the below code:
Array(2).map((val) => console.log(val))
.. nothing happens, but when I spread out the elements of the array into an other array, like so:
[...Array(2)].map((val) => console.log(val))
.. I get undefined console logs. Can anyone please help me understand what is happening here ?
Without spreading, the array is simply an object with a length property, with no items populated:
{ length: 2, proto: Array(0) }
When you spread (...) into an array, if the object you're spreading has a length property, it will spread that many items, starting from obj[0], obj[1], etc. The object in question doesn't actually need to have those numeric properties - if it doesn't, then the result of the spreading will be an array of undefineds:
console.log([...Array(2)]);
An array of undefineds can be iterated over - here, the indicies 0 and 1 do exist in the array object, they just happen to not be assigned to anything.
const arr = [undefined, undefined];
console.log(1 in arr);
So, in short, it's because the difference between an array such as
{ length: 2 } // no elements to iterate over
and
{ length: 2, 0: undefined, 1: undefined }
Note that you shouldn't use map if you're not using the transformed array that .map returns - otherwise, if you just want to iterate over each item and console.log it, for example, you should use forEach.
This is because the spread operator takes the iterable's length, then iterates over the passed value (See spec).
Since all returned valued from iterating over Array(2) are undefined, your second line maps over [undefined, undefined], while your fist line maps over [empty x 2]. Note that "empty" aren't actual values.
map doesn't iterate over "empty" entries, like those created by Array(2), but does iterate over undefined values:
Because Array(2) doesn't populate the array. But it does set the length property to 2. So in the first instance, map has nothing to iterate over. But the destructuring creates an array of 2 elements of undefined, which allows map to work since undefined is a valid primitive.
This is how Array.map works - please read here: Array.prototype.map()
callback is invoked only for indexes of the array which have assigned
values, including undefined. It is not called for missing elements of
the array (that is, indexes that have never been set, which have been
deleted or which have never been assigned a value).

Allocate new array in javascript

Trying to allocate new array with values.
Case 1 :
var x = new Array(3).map(()=>1);
Now x is [undefined * 3]
Case2 :
var x = [...new Array(3)].map(()=>1);
And now x is [1,1,1]
Can someone help here?
Why using this spread operator makes such a difference?
And why Case 1 doesn't work ?
tl;dr: The first array has holes, the second one doesn't. .map skips holes.
By using a spread element, the array is treated as an iterable, i.e. you get an iterator to iterate over the array. The iterator basically works like a for loop, it will iterate the array from index 0 to index array.length - 1 (see the spec for details), and add the value at each index to the new array. Since your array doesn't contain any values, array[i] will return undefined for every index.
That means, [...new Array(3)] results in an array that literally contains three undefined values, as opposed to just a "spare" array of length 3.
See the difference in Chrome:
We call "sparse arrays" also "arrays with holes".
Here is the crux: Many array methods, including .map, skip holes! They are not treating the hole as the value undefined, the completely ignore it.
You can easily verify that by putting a console.log in the .map callback:
Array(3).map(() => console.log('call me'));
// no output
And that's the reason your first example doesn't work. You have a sparse array with only holes, which .map ignores. Creating a new array with the spread element creates an array without holes, hence .map works.
Array
arrayLength
If the only argument passed to the Array constructor is an integer between 0 and 232-1 (inclusive), this returns a new
JavaScript array with length set to that number.
new Array(3) does not actually create iterable values at created array having .length property set to 3.
See also Undefined values with new Array() in JavaScript .
You can use Array.from() at first example to return expected results
var x = Array.from(Array(3)).map(()=>1);
Spread operator
The spread operator allows an expression to be expanded in places
where multiple arguments (for function calls) or multiple elements
(for array literals) or multiple variables (for destructuring
assignment) are expected.
var x = [...new Array(10)].map(()=>1);
creates an array having values undefined and .length set to 10 from Array(10), which is iterable at .map()
Why case 1 doesn't work ?
Map function calls callback function in each element in ascending order
In your first case (breaking down..),
var x = new Array(3);
x = x.map( () => 1 );
x is an array with uninitialized index. Therefore, map function does not know where to start iterating your array from. And causing it to not iterating it at all (Which is not working).
You can test it by (in Chrome),
var x = new Array(5);
// This will display '[undefined x 5]' in chrome, because their indexes are uninitialized
x[1] = undefined;
// '[undefined x 1, undefined, undefined x 3]' Only array[1] that has its index.
// And you can use 'map' function to change its value.
x = x.map( () => 1 );
// '[undefined x 1, 1, undefined x 3]'
Why using this spread operator makes such a difference?
In your second sample,
Spread operator allows parts of an array literal to be initialized from an iterable expression
And enclose it in square bracket to properly use it.
So, [...new Array(2)] is actually an indexed array of [undefined, undefined].
Since your array in the following sample has been indexed. Let's have a look (in Chrome),
var x = [...new Array(2)];
// Now 'x' is an array with indexes [undefined, undefined]
x = x.map( () => 1 );
// Will return [1, 1]
Now, each value in x has its own indexes. Then finally, map function is able to iterate over it and call the callback function for each element in ascending order.

Array.apply(null, Array(9)) vs new Array(9) [duplicate]

What exactly is the difference between:
Array(3)
// and
Array.apply(null, Array(3) )
The first returns [undefined x 3] while the second returns [undefined, undefined, undefined]. The second is chainable through Array.prototype.functions such as .map, but the first isn't. Why?
There is a difference, a quite significant one.
The Array constructor either accepts one single number, giving the lenght of the array, and an array with "empty" indices is created, or more correctly the length is set but the array doesn't really contain anything
Array(3); // creates [], with a length of 3
When calling the array constructor with a number as the only argument, you create an array that is empty, and that can't be iterated with the usual Array methods.
Or... the Array constructor accepts several arguments, whereas an array is created where each argument is a value in the array
Array(1,2,3); // creates an array [1,2,3] etc.
When you call this
Array.apply(null, Array(3) )
It get's a little more interesting.
apply accepts the this value as the first argument, and as it's not useful here, it's set to null
The interesting part is the second argument, where an empty array is being passed in.
As apply accepts an array it would be like calling
Array(undefined, undefined, undefined);
and that creates an array with three indices that's not empty, but have the value actually set to undefined, which is why it can be iterated over.
TL;DR
The main difference is that Array(3) creates an array with three indices that are empty. In fact, they don't really exist, the array just have a length of 3.
Passing in such an array with empty indices to the Array constructor using apply is the same as doing Array(undefined, undefined, undefined);, which creates an array with three undefined indices, and undefined is in fact a value, so it's not empty like in the first example.
Array methods like map() can only iterate over actual values, not empty indices.
The .map() API does not iterate over completely uninitialized array elements. When you make a new array with the new Array(n) constructor, you get an array with the .length you asked for but with non-existent elements that will be skipped by methods like .map().
The expression Array.apply(null, Array(9)) explicitly populates the newly-created array instance with undefined, but that's good enough. The trick is whether or not the in operator will report that the array contains an element at the given index. That is:
var a = new Array(9);
alert(2 in a); // alerts "false"
That's because there really is no element at position 2 in the array. But:
var a = Array.apply(null, Array(9));
alert(2 in a); // alerts "true"
The outer call to the Array constructor will have explicitly populated the elements.
This is an artifact of how apply works. When you do:
new Array(9)
an empty array is created with a length of 9. map does not visit non–existent members, so does nothing at all. However, apply turns the array into a list using CreateListFromArrayLike so it turns the formerly empty array into a parameter list like:
[undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined];
that is passed to Array to create an array with 9 members, all with a value of undefined. So now map will visit them all.
BTW, ECMAScript 2015 has Array.prototype.fill for this (also see MDN) so you can do:
Array(9).fill(0);
Because the first array would not have ordered properties arr[0] === undefined and the second does. Array functions like forEach and map will iterate from 0 to the array's length - 1 and the lack of order to the properties of the first is an issue. The second version produces an array with the correct ordering, i.e.
arr = Array.apply(null, Array(3));
arr[0] === undefined //true
arr[1] === undefined //true
//etc.
The first version as you noticed doesn't. Also, adding new to the first version would not make it work.
In the first case you have one operation
Array(3)
Its creates an array with three empty slots. Not an array with the three undefined values but exactly - empty.
At the second case
Array.apply(null, Array(3) )
we can spread it to the three operations:
first: Array(3) - you get an array with 3 empty slots;
second: Array(3) spreads by Function.prototype.apply() function to 3 parameters that it passes to Array() function. At this stage 3 empty slots in given array transformes by apply() to 3 undefined values (it looks like if apply() sees an empty slot it automaticaly turns it to undefined in any sparsed array).
third: we get an Array(undefined, undefined, undefined). And that will do to us an array with 3 undefined (not empty) values.
Because now you have 3 undefined but not empty slots, you can use them with map() function.
Note that not only Function.prototype.apply() have such behavior of decomposing arrays by such way. You can also do this in ECMAScript 6 by "..." - spread operator.
Array(...new Array(3));
This will also returns an array with 3 undefined and respectively can be mapped slots.
Here i giving more detailed explanation.
https://stackoverflow.com/a/56814230/11715665

Categories

Resources