Related
I've observed this in Firefox-3.5.7/Firebug-1.5.3 and Firefox-3.6.16/Firebug-1.6.2
When I fire up Firebug:
var x = new Array(3)
console.log(x)
// [undefined, undefined, undefined]
var y = [undefined, undefined, undefined]
console.log(y)
// [undefined, undefined, undefined]
console.log( x.constructor == y.constructor) // true
console.log(
x.map(function() { return 0; })
)
// [undefined, undefined, undefined]
console.log(
y.map(function() { return 0; })
)
// [0, 0, 0]
What's going on here? Is this a bug, or am I misunderstanding how to use new Array(3)?
I had a task that I only knew the length of the array and needed to transform the items.
I wanted to do something like this:
let arr = new Array(10).map((val,idx) => idx);
To quickly create an array like this:
[0,1,2,3,4,5,6,7,8,9]
But it didn't work because:
(see Jonathan Lonowski's answer)
The solution could be to fill up the array items with any value (even with undefined) using Array.prototype.fill()
let arr = new Array(10).fill(undefined).map((val,idx) => idx);
console.log(new Array(10).fill(undefined).map((val, idx) => idx));
Update
Another solution could be:
let arr = Array.apply(null, Array(10)).map((val, idx) => idx);
console.log(Array.apply(null, Array(10)).map((val, idx) => idx));
It appears that the first example
x = new Array(3);
Creates an array with a length of 3 but without any elements, so the indices [0], [1] and [2] is not created.
And the second creates an array with the 3 undefined objects, in this case the indices/properties them self are created but the objects they refer to are undefined.
y = [undefined, undefined, undefined]
// The following is not equivalent to the above, it's the same as new Array(3)
y = [,,,];
As map runs on the list of indices/properties, not on the set length, so if no indices/properties is created, it will not run.
With ES6, you can do [...Array(10)].map((a, b) => a) , quick and easy!
From the MDC page for map:
[...] callback is invoked only for indexes of the array which have assigned value; [...]
[undefined] actually applies the setter on the index(es) so that map will iterate, whereas new Array(1) just initializes the index(es) with a default value of undefined so map skips it.
I believe this is the same for all iteration methods.
ES6 solution:
[...Array(10)]
Doesn't work on typescript (2.3), though
The arrays are different. The difference is that new Array(3) creates an array with a length of three but no properties, while [undefined, undefined, undefined] creates an array with a length of three and three properties called "0", "1" and "2", each with a value of undefined. You can see the difference using the in operator:
"0" in new Array(3); // false
"0" in [undefined, undefined, undefined]; // true
This stems from the slightly confusing fact that if you try to get the value of a non-existent property of any native object in JavaScript, it returns undefined (rather than throwing an error, as happens when you try to refer to a non-existent variable), which is the same as what you get if the property has previously been explictly set to undefined.
For reasons thoroughly explained in other answers, Array(n).map doesn't work. However, in ES2015 Array.from accepts a map function:
let array1 = Array.from(Array(5), (_, i) => i + 1)
console.log('array1', JSON.stringify(array1)) // 1,2,3,4,5
let array2 = Array.from({length: 5}, (_, i) => (i + 1) * 2)
console.log('array2', JSON.stringify(array2)) // 2,4,6,8,10
In ECMAScript 6th edition specification.
new Array(3) only define property length and do not define index properties like {length: 3}. see https://www.ecma-international.org/ecma-262/6.0/index.html#sec-array-len Step 9.
[undefined, undefined, undefined] will define index properties and length property like {0: undefined, 1: undefined, 2: undefined, length: 3}. see https://www.ecma-international.org/ecma-262/6.0/index.html#sec-runtime-semantics-arrayaccumulation ElementList Step 5.
methods map, every, some, forEach, slice, reduce, reduceRight, filter of Array will check the index property by HasProperty internal method, so new Array(3).map(v => 1) will not invoke the callback.
for more detail, see https://www.ecma-international.org/ecma-262/6.0/index.html#sec-array.prototype.map
How to fix?
let a = new Array(3);
a.join('.').split('.').map(v => 1);
let a = new Array(3);
a.fill(1);
let a = new Array(3);
a.fill(undefined).map(v => 1);
let a = new Array(3);
[...a].map(v => 1);
I think the best way to explain this is to look at the way that Chrome handles it.
>>> x = new Array(3)
[]
>>> x.length
3
So what is actually happening is that new Array() is returning an empty array that has a length of 3, but no values. Therefore, when you run x.map on a technically empty array, there is nothing to be set.
Firefox just 'fills in' those empty slots with undefined even though it has no values.
I don't think this is explicitly a bug, just a poor way of representing what is going on. I suppose Chrome's is "more correct" because it shows that there isn't actually anything in the array.
Not a bug. That's how the Array constructor is defined to work.
From MDC:
When you specify a single numeric parameter with the Array constructor, you specify the initial length of the array. The following code creates an array of five elements:
var billingMethod = new Array(5);
The behavior of the Array constructor depends on whether the single parameter is a number.
The .map() method only includes in the iteration elements of the array that have explicitly had values assigned. Even an explicit assignment of undefined will cause a value to be considered eligible for inclusion in the iteration. That seems odd, but it's essentially the difference between an explicit undefined property on an object and a missing property:
var x = { }, y = { z: undefined };
if (x.z === y.z) // true
The object x does not have a property called "z", and the object y does. However, in both cases it appears that the "value" of the property is undefined. In an array, the situation is similar: the value of length does implicitly perform a value assignment to all the elements from zero through length - 1. The .map() function therefore won't do anything (won't call the callback) when called on an array newly constructed with the Array constructor and a numeric argument.
Just ran into this. It sure would be convenient to be able to use Array(n).map.
Array(3) yields roughly {length: 3}
[undefined, undefined, undefined] creates the numbered properties:
{0: undefined, 1: undefined, 2: undefined, length: 3}.
The map() implementation only acts on defined properties.
If you are doing this in order to easily fill up an array with values, can't use fill for browser support reasons and really don't want to do a for-loop, you can also do x = new Array(3).join(".").split(".").map(... which will give you an array of empty strings.
Quite ugly I have to say, but at least the problem and intention are quite clearly communicated.
Since the question is why, this has to do with how JS was designed.
There are 2 main reasons I can think of to explain this behavior:
Performance: Given x = 10000 and new Array(x) it is wise for the constructor to avoid looping from 0 to 10000 to fill the array with undefined values.
Implicitly "undefined": Give a = [undefined, undefined] and b = new Array(2), a[1] and b[1] will both return undefined, but a[8] and b[8] will also return undefined even if they're out of range.
Ultimately, the notation empty x 3 is a shortcut to avoid setting and displaying a long list of undefined values that are undefined anyway because they are not declared explicitly.
Note: Given array a = [0] and a[9] = 9, console.log(a) will return (10) [0, empty x 8, 9], filling the gap automatically by returning the difference between the two values declared explicitly.
Here's a simple utility method as a workaround:
Simple mapFor
function mapFor(toExclusive, callback) {
callback = callback || function(){};
var arr = [];
for (var i = 0; i < toExclusive; i++) {
arr.push(callback(i));
}
return arr;
};
var arr = mapFor(3, function(i){ return i; });
console.log(arr); // [0, 1, 2]
arr = mapFor(3);
console.log(arr); // [undefined, undefined, undefined]
Complete Example
Here's a more complete example (with sanity checks) which also allows specifying an optional starting index:
function mapFor() {
var from, toExclusive, callback;
if (arguments.length == 3) {
from = arguments[0];
toExclusive = arguments[1];
callback = arguments[2];
} else if (arguments.length == 2) {
if (typeof arguments[1] === 'function') {
from = 0;
toExclusive = arguments[0];
callback = arguments[1];
} else {
from = arguments[0];
toExclusive = arguments[1];
}
} else if (arguments.length == 1) {
from = 0;
toExclusive = arguments[0];
}
callback = callback || function () {};
var arr = [];
for (; from < toExclusive; from++) {
arr.push(callback(from));
}
return arr;
}
var arr = mapFor(1, 3, function (i) { return i; });
console.log(arr); // [1, 2]
arr = mapFor(1, 3);
console.log(arr); // [undefined, undefined]
arr = mapFor(3);
console.log(arr); // [undefined, undefined, undefined]
Counting Down
Manipulating the index passed to the callback allows counting backwards:
var count = 3;
var arr = arrayUtil.mapFor(count, function (i) {
return count - 1 - i;
});
// arr = [2, 1, 0]
I have some code here and I was wondering if it is the same thing or different. I am pretty sure these are both suppose to be the same but I wasnt sure if I was doing it right.
let zoneComment = updatedMap[action.comment.zone]
? [...updatedMap[action.comment.zone]] : [];
let zoneComment = updatedMap[action.comment.zone]
? Object.assign([], updatedMap[action.comment.zone]) : [];
If these are the same then which should I use or does it matter? I want to use best practice so if it is your OPINION of which is better then please state so.
In your particular case they are not the same.
The reason is that you have an array, not an object.
Doing ... on an array will spread out all the elements in the array (but not the properties)
Doing Object.assign expects an object so it will treat an array as an object and copy all enumerable own properties into it, not just the elements:
const a = [1, 2, 3];
a.test = 'example';
const one = [...a] // [1, 2, 3];
const two = Object.assign([], a); // { '0': 1, '1': 2, '2': 3, 'test': 'example' }
console.log('\none');
for (let prop in one) {
console.log(prop);
}
console.log('\ntwo');
for (let prop in two) {
console.log(prop);
}
However, if you compare the ... operator applied on an object with Object.assign, they are essentially the same:
// same result
const a = { name: 'test' }
console.log({ ...a })
console.log(Object.assign({}, a))
except ... always creates a new object but Object.assign also allows you to mutate an existing object.
// same result
const a = { name: 'test' }
const b = { ...a, name: 'change' };
console.log(a.name); // test
Object.assign(a, { name: 'change'})
console.log(a.name); // change
Keep in mind that Object.assign is already a part of the language whereas object spread is still only a proposal and would require a preprocessing step (transpilation) with a tool like babel.
To make it short, always use ... spread construction and never Object.assign on arrays.
Object.assign is intended for objects. Although arrays are objects, too, it will cause a certain effect on them which is useful virtually never.
Object.assign(obj1, obj2) gets values from all enumerable keys from obj2 and assigns them to obj1. Arrays are objects, and array indexes are object keys, in fact.
[...[1, 2, 3], ...[4, 5]] results in [1, 2, 3, 4, 5] array.
Object.assign([1, 2, 3], [4, 5]) results in [4, 5, 3] array, because values on 0 and 1 indexes in first array are overwritten with values from second array.
In the case when first array is empty, Object.assign([], arr) and [...arr] results are similar. However, the proper ES5 alternative to [...arr] is [].concat(arr) and not Object.assign([], arr).
Your question really bubbles down to:
Are [...arr] and Object.assign([], arr) providing the same result when arr is an array?
The answer is: usually, yes, but:
if arr is a sparse array that has no value for its last slot, then the length property of the result will not be the same in both cases: the spread syntax will maintain the same value for the length property, but Object.assign will produce an array with a length that corresponds to the index of the last used slot, plus one.
if arr is a sparse array (like what you get with Array(10)) then the spread syntax will create an array with undefined values at those indexes, so it will not be a sparse array. Object.assign on the other hand, will really keep those slots empty (non-existing).
if arr has custom enumerable properties, they will be copied by Object.assign, but not by the spread syntax.
Here is a demo of the first two of those differences:
var arr = ["abc"]
arr[2] = "def"; // leave slot 1 empty
arr.length = 4; // empty slot at index 3
var a = [...arr];
var b = Object.assign([], arr);
console.log(a.length, a instanceof Array); // 4, true
console.log(b.length, b instanceof Array); // 3, true
console.log('1' in arr); // false
console.log('1' in a); // true (undefined)
console.log('1' in b); // false
If however arr is a standard array (with no extra properties) and has all its slots filled, then both ways produce the same result:
Both return an array. [...arr] does this by definition, and Object.assign does this because its first argument is an array, and it is that object that it will return: mutated, but it's proto will not change. Although length is not an enumerable property, and Object.assign will not copy it, the behaviour of the first-argument array is that it will adapt its length attribute as the other properties are assigned to it.
Both take shallow copies.
Conclusion
If your array has custom properties you want to have copied, and it has no empty slots at the end: use Object.assign.
If your array has no custom properties (or you don't care about them) and does not have empty slots: use the spread syntax.
If your array has custom properties you want to have copied, and empty slots you want to maintain: neither method will do both of this. But with Object.assign it is easier to accomplish:
a = Object.assign([], arr, { length: arr.length });
One would think that in JavaScript:
var array = [1,2,undefined,4];
is the same as:
var array = [1,2];
array.length = 3;
array.push(4);
but it's not. This code shows it:
var array1 = [1,2];
array1.length = 3;
array1.push(4);
var array2 = [1,2,undefined,4];
traverseArray(array1);
traverseArray(array2);
function traverseArray(array) {
console.log("trying: " + array);
console.log("reduce:");
array.reduce(function(prev, current, index, array) {
if(current === undefined) {
console.log("Found undefined");
}
console.log(index+": " +current);
}, undefined);
console.log("for loop:")
for(var i=0;i < array.length;i++) {
var current = array[i];
console.log(i+": " +current);
}
console.log();
}
Output:
trying: 1,2,,4
reduce:
0: 1
1: 2
3: 4
for loop:
0: 1
1: 2
2: undefined
3: 4
trying: 1,2,,4
reduce:
0: 1
1: 2
Found undefined
2: undefined
3: 4
for loop:
0: 1
1: 2
2: undefined
3: 4
Why is undefined in array1 not the same as undefined in array2 and why does the for loop act the same but reduce does not?
array1 has three numerically-named properties: 0, 1, and 3.
array2 has four numerically-named properties: 0, 1, 2, and 3. The value of the property named 2 happens to be undefined.
When you ask an object for the value of a property it doesn't have, the result is undefined.
In the for loop, you ask each array for the values of its properties named 0, 1, 2, and 3. For array1, the property named 2 does not exist, so the property access produces undefined. For array2, the property does exist, but its value actually is undefined, so you get the same result.
On the other hand, reduce only operates on properties that actually exist. From the ECMAScript specification, this is how reduce loops over arrays, using a counter k:
Repeat, while k < len
Let Pk be ToString(k).
Let kPresent be the result of calling the [[HasProperty]] internal method of O with argument Pk.
If kPresent is true, then... [use the value at index k for the reduce call]
So, we can see that an index is only used if passes a [[HasProperty]] check. array1 does not have a property named 2, so that index is skipped.
apsillers nailed it in the comments . . . the difference is that in array2 you have actually assigned an undefined value to the 3rd element in the array (i.e., at index 2), where as, in array1, you have two elements initially, change the length property, and then add a third element in the forth position.
Here are the relevent sections from MDN that explains why the distinction is important:
From the page on Array.length:
When you extend an array by changing its length property, the number of actual elements does not increase; for example, if you set length to 3 when it is currently 2, the array still contains only 2 elements. Thus, the length property says nothing about the number of defined values in the array.
From the page on Array.push():
The push method relies on a length property to determine where to start inserting the given values.
The key here, is really that the length property is really, simple a property that is in no way inherantly tied to the contents of the array. It is simply because the various methods of Array also happen to maintain that property, as they "do their work", that I behaves as if it is.
So, as a result, in array2, the code is actually reporting back the undefined value that you assigned to array2[2], whereas, with array1, the code is interpreting the absence of a value at array1[2] as undefined.
This question already has answers here:
Undefined values in Array(len) initializer
(5 answers)
Closed 7 years ago.
I am confused by the results of mapping over an array created with new:
function returnsFourteen() {
return 14;
}
var a = new Array(4);
> [undefined x 4] in Chrome, [, , , ,] in Firefox
a.map(returnsFourteen);
> [undefined x 4] in Chrome, [, , , ,] in Firefox
var b = [undefined, undefined, undefined, undefined];
> [undefined, undefined, undefined, undefined]
b.map(returnsFourteen);
> [14, 14, 14, 14]
I expected a.map(returnsFourteen) to return [14, 14, 14, 14] (the same as b.map(returnsFourteen), because according to the MDN page on arrays:
If the only argument passed to the Array constructor is an integer
between 0 and 2**32-1 (inclusive), a new JavaScript array is created
with that number of elements.
I interpret that to mean that a should have 4 elements.
What am I missing here?
When you create an array like so:
var arr1 = new Array( 4 );
you get an array that has a length of 4, but that has no elements. That's why map doesn't tranform the array - the array has no elements to be transformed.
On the other hand, if you do:
var arr2 = [ undefined, undefined, undefined, undefined ];
you get and array that also has a length of 4, but that does have 4 elements.
Notice the difference between having no elements, and having elements which values are undefined. Unfortunately, the property accessor expression will evaluate to the undefined value in both cases, so:
arr1[0] // undefined
arr2[0] // undefined
However, there is a way to differentiate these two arrays:
'0' in arr1 // false
'0' in arr2 // true
var a = new Array(4);
This defines a new array object with an explicit length of 4, but no elements.
var b = [undefined, undefined, undefined, undefined];
This defines a new array object with an implicit length of 4, with 4 elements, each with the value undefined.
From the docs:
callback is invoked only for indexes of the array which have assigned
values; it is not invoked for indexes which have been deleted or which
have never been assigned values.
For array a, there are no elements that have been assigned values, so it does nothing.
For array b, there are four elements that have been assigned values (yes, undefined is a value), so it maps all four elements to the number 14.
new Array(len) creates an empty array, and does something different than filling it with undefined values: It sets its length to len. So, it translates to this code:
var newArr = [];
newArr.length = len;
Let's have some fun with newArr (assuming that len = 4):
newArr.length; //4
newArr[1] === undefined; //true
newArr.hasOwnProperty(1); //false
This is because while the is 4 items long, it does not contain any of these 4 items. Imagine an empty bullet-clip: It has space for, say, 20 bullets, but it doesn't contain any of them. They weren't even set to the value undefined, they just are...undefined (which is a bit confusing.)
Now, Array.prototype.map happily walks along your first array, chirping and whistling, and every time it sees an array item, it calls a function on it. But, as it walks along the empty bullet-clip, it sees no bullets. Sure, there are room for bullets, but that doesn't make them exist. In here, there is no value, because the key which maps to that value does not exist.
For the second array, which is filled with undefined values, the value is undefined, and so is the key. There is something inside b[1] or b[3], but that something isn't defined; but Array.prototype.map doesn't care, it'll operate on any value, as long as it has a key.
For further inspection in the spec:
new Array(len) : http://es5.github.com/#x15.4.2.2
Array.prototype.map : http://es5.github.com/#x15.4.4.19 (pay close attention to step 8.b)
One additional answer on the behavior of console.log. Plain simple, the output is sugar and technically wrong.
Lets consider this example:
var foo = new Array(4),
bar = [undefined, undefined, undefined, undefined];
console.log( Object.getOwnPropertyNames(bar) );
console.log( Object.getOwnPropertyNames(foo) );
As we can see in the result, the .getOwnPropertyNames function returns
["length"]
for the foo Array/Object, but
["length", "0", "1", "2", "3"]
for the bar Array/Object.
So, the console.log just fools you on outputting Arrays which just have a defined .length but no real property assignments.
I've run into the following code in the es-discuss mailing list:
Array.apply(null, { length: 5 }).map(Number.call, Number);
This produces
[0, 1, 2, 3, 4]
Why is this the result of the code? What's happening here?
Understanding this "hack" requires understanding several things:
Why we don't just do Array(5).map(...)
How Function.prototype.apply handles arguments
How Array handles multiple arguments
How the Number function handles arguments
What Function.prototype.call does
They're rather advanced topics in javascript, so this will be more-than-rather long. We'll start from the top. Buckle up!
1. Why not just Array(5).map?
What's an array, really? A regular object, containing integer keys, which map to values. It has other special features, for instance the magical length variable, but at it's core, it's a regular key => value map, just like any other object. Let's play with arrays a little, shall we?
var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined
//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']
We get to the inherent difference between the number of items in the array, arr.length, and the number of key=>value mappings the array has, which can be different than arr.length.
Expanding the array via arr.length does not create any new key=>value mappings, so it's not that the array has undefined values, it does not have these keys. And what happens when you try to access a non-existent property? You get undefined.
Now we can lift our heads a little, and see why functions like arr.map don't walk over these properties. If arr[3] was merely undefined, and the key existed, all these array functions would just go over it like any other value:
//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';
arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']
arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]
I intentionally used a method call to further prove the point that the key itself was never there: Calling undefined.toUpperCase would have raised an error, but it didn't. To prove that:
arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined
And now we get to my point: How Array(N) does things. Section 15.4.2.2 describes the process. There's a bunch of mumbo jumbo we don't care about, but if you manage to read between the lines (or you can just trust me on this one, but don't), it basically boils down to this:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(operates under the assumption (which is checked in the actual spec) that len is a valid uint32, and not just any number of value)
So now you can see why doing Array(5).map(...) wouldn't work - we don't define len items on the array, we don't create the key => value mappings, we simply alter the length property.
Now that we have that out of the way, let's look at the second magical thing:
2. How Function.prototype.apply works
What apply does is basically take an array, and unroll it as a function call's arguments. That means that the following are pretty much the same:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Now, we can ease the process of seeing how apply works by simply logging the arguments special variable:
function log () {
console.log(arguments);
}
log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
//["mary", "had", "a", "little", "lamb"]
//arguments is a pseudo-array itself, so we can use it as well
(function () {
log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
//["mary", "had", "a", "little", "lamb"]
//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
//[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]
//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!
log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]
It's easy to prove my claim in the second-to-last example:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(yes, pun intended). The key => value mapping may not have existed in the array we passed over to apply, but it certainly exists in the arguments variable. It's the same reason the last example works: The keys do not exist on the object we pass, but they do exist in arguments.
Why is that? Let's look at Section 15.3.4.3, where Function.prototype.apply is defined. Mostly things we don't care about, but here's the interesting portion:
Let len be the result of calling the [[Get]] internal method of argArray with argument "length".
Which basically means: argArray.length. The spec then proceeds to do a simple for loop over length items, making a list of corresponding values (list is some internal voodoo, but it's basically an array). In terms of very, very loose code:
Function.prototype.apply = function (thisArg, argArray) {
var len = argArray.length,
argList = [];
for (var i = 0; i < len; i += 1) {
argList[i] = argArray[i];
}
//yeah...
superMagicalFunctionInvocation(this, thisArg, argList);
};
So all we need to mimic an argArray in this case is an object with a length property. And now we can see why the values are undefined, but the keys aren't, on arguments: We create the key=>value mappings.
Phew, so this might not have been shorter than the previous part. But there'll be cake when we finish, so be patient! However, after the following section (which'll be short, I promise) we can begin dissecting the expression. In case you forgot, the question was how does the following work:
Array.apply(null, { length: 5 }).map(Number.call, Number);
3. How Array handles multiple arguments
So! We saw what happens when you pass a length argument to Array, but in the expression, we pass several things as arguments (an array of 5 undefined, to be exact). Section 15.4.2.1 tells us what to do. The last paragraph is all that matters to us, and it's worded really oddly, but it kind of boils down to:
function Array () {
var ret = [];
ret.length = arguments.length;
for (var i = 0; i < arguments.length; i += 1) {
ret[i] = arguments[i];
}
return ret;
}
Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]
Tada! We get an array of several undefined values, and we return an array of these undefined values.
The first part of the expression
Finally, we can decipher the following:
Array.apply(null, { length: 5 })
We saw that it returns an array containing 5 undefined values, with keys all in existence.
Now, to the second part of the expression:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
This will be the easier, non-convoluted part, as it doesn't so much rely on obscure hacks.
4. How Number treats input
Doing Number(something) (section 15.7.1) converts something to a number, and that is all. How it does that is a bit convoluted, especially in the cases of strings, but the operation is defined in section 9.3 in case you're interested.
5. Games of Function.prototype.call
call is apply's brother, defined in section 15.3.4.4. Instead of taking an array of arguments, it just takes the arguments it received, and passes them forward.
Things get interesting when you chain more than one call together, crank the weird up to 11:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
This is quite wtf worthy until you grasp what's going on. log.call is just a function, equivalent to any other function's call method, and as such, has a call method on itself as well:
log.call === log.call.call; //true
log.call === Function.call; //true
And what does call do? It accepts a thisArg and a bunch of arguments, and calls its parent function. We can define it via apply (again, very loose code, won't work):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Let's track how this goes down:
log.call.call(log, {a:4}, {a:5});
this = log.call
thisArg = log
args = [{a:4}, {a:5}]
log.call.apply(log, [{a:4}, {a:5}])
log.call({a:4}, {a:5})
this = log
thisArg = {a:4}
args = [{a:5}]
log.apply({a:4}, [{a:5}])
The later part, or the .map of it all
It's not over yet. Let's see what happens when you supply a function to most array methods:
function log () {
console.log(this, arguments);
}
var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^ ^-----------------------^
// this arguments
If we don't provide a this argument ourselves, it defaults to window. Take note of the order in which the arguments are provided to our callback, and let's weird it up all the way to 11 again:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
Whoa whoa whoa...let's back up a bit. What's going on here? We can see in section 15.4.4.18, where forEach is defined, the following pretty much happens:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
So, we get this:
log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);
Now we can see how .map(Number.call, Number) works:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Which returns the transformation of i, the current index, to a number.
In conclusion,
The expression
Array.apply(null, { length: 5 }).map(Number.call, Number);
Works in two parts:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
The first part creates an array of 5 undefined items. The second goes over that array and takes its indices, resulting in an array of element indices:
[0, 1, 2, 3, 4]
Disclaimer: This is a very formal description of the above code - this is how I know how to explain it. For a simpler answer - check Zirak's great answer above. This is a more in depth specification in your face and less "aha".
Several things are happening here. Let's break it up a bit.
var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
In the first line, the array constructor is called as a function with Function.prototype.apply.
The this value is null which does not matter for the Array constructor (this is the same this as in the context according to 15.3.4.3.2.a.
Then new Array is called being passed an object with a length property - that causes that object to be an array like for all it matters to .apply because of the following clause in .apply:
Let len be the result of calling the [[Get]] internal method of argArray with argument "length".
As such, .apply is passing arguments from 0 to .length , since calling [[Get]] on { length: 5 } with the values 0 to 4 yields undefined the array constructor is called with five arguments whose value is undefined (getting an undeclared property of an object).
The array constructor is called with 0, 2 or more arguments.
The length property of the newly constructed array is set to the number of arguments according to the specification and the values to the same values.
Thus var arr = Array.apply(null, { length: 5 }); creates a list of five undefined values.
Note: Notice the difference here between Array.apply(0,{length: 5}) and Array(5), the first creating five times the primitive value type undefined and the latter creating an empty array of length 5. Specifically, because of .map's behavior (8.b) and specifically [[HasProperty] .
So the code above in a compliant specification is the same as:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Now off to the second part.
Array.prototype.map calls the callback function (in this case Number.call) on each element of the array and uses the specified this value (in this case setting the this value to `Number).
The second parameter of the callback in map (in this case Number.call) is the index, and the first is the this value.
This means that Number is called with this as undefined (the array value) and the index as the parameter. So it's basically the same as mapping each undefined to its array index (since calling Number performs type conversion, in this case from number to number not changing the index).
Thus, the code above takes the five undefined values and maps each to its index in the array.
Which is why we get the result to our code.
As you said, the first part:
var arr = Array.apply(null, { length: 5 });
creates an array of 5 undefined values.
The second part is calling the map function of the array which takes 2 arguments and returns a new array of the same size.
The first argument which map takes is actually a function to apply on each element in the array, it is expected to be a function which takes 3 arguments and returns a value.
For example:
function foo(a,b,c){
...
return ...
}
if we pass the function foo as the first argument it will be called for each element with
a as the value of the current iterated element
b as the index of the current iterated element
c as the whole original array
The second argument which map takes is being passed to the function which you pass as the first argument. But it would not be a, b, nor c in case of foo, it would be this.
Two examples:
function bar(a,b,c){
return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]
function baz(a,b,c){
return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]
and another one just to make it clearer:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
So what about Number.call ?
Number.call is a function that takes 2 arguments, and tries to parse the second argument to a number (I'm not sure what it does with the first argument).
Since the second argument that map is passing is the index, the value that will be placed in the new array at that index is equal to the index. Just like the function baz in the example above. Number.call will try to parse the index - it will naturally return the same value.
The second argument you passed to the map function in your code doesn't actually have an effect on the result. Correct me if I'm wrong, please.
An array is simply an object comprising the 'length' field and some methods (e.g. push). So arr in var arr = { length: 5} is basically the same as an array where the fields 0..4 have the default value which is undefined (i.e. arr[0] === undefined yields true).
As for the second part, map, as the name implies, maps from one array to a new one. It does so by traversing through the original array and invoking the mapping-function on each item.
All that's left is to convince you that the result of mapping-function is the index. The trick is to use the method named 'call'(*) which invokes a function with the small exception that the first param is set to be the 'this' context, and the second becomes the first param (and so on). Coincidentally, when the mapping-function is invoked, the second param is the index.
Last but not least, the method which is invoked is the Number "Class", and as we know in JS, a "Class" is simply a function, and this one (Number) expects the first param to be the value.
(*) found in Function's prototype (and Number is a function).
MASHAL