Sparse arrays and reduce in JavaScript - javascript

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.

Related

Why is Array(10) != [...Array(10)] [duplicate]

I can use Array() to have an array with a fixed number of undefined entries. For example
Array(2); // [empty × 2]
But if I go and use the map method, say, on my new array, the entries are still undefined:
Array(2).map( () => "foo"); // [empty × 2]
If I copy the array then map does work:
[...Array(2)].map( () => "foo"); // ["foo", "foo"]
Why do I need a copy to use the array?
When you use Array(arrayLength) to create an array, you will have:
a new JavaScript array with its length property set to that number (Note: this implies an array of arrayLength empty slots, not slots with actual undefined values).
The array does not actually contain any values, not even undefined values - it simply has a length property.
When you spread an iterable object with a length property into an array, spread syntax accesses each index and sets the value at that index in the new array. For example:
const arr1 = [];
arr1.length = 4;
// arr1 does not actually have any index properties:
console.log('1' in arr1);
const arr2 = [...arr1];
console.log(arr2);
console.log('2' in arr2);
And .map only maps properties/values for which the property actually exists in the array you're mapping over.
Using the array constructor is confusing. I would suggest using Array.from instead, when creating arrays from scratch - you can pass it an object with a length property, as well as a mapping function:
const arr = Array.from(
{ length: 2 },
() => 'foo'
);
console.log(arr);
The reason is that the array element is unassigned. See here the first paragraph of the description. ... callback is invoked only for indexes of the array which have assigned values, including undefined.
Consider:
var array1 = Array(2);
array1[0] = undefined;
// pass a function to map
const map1 = array1.map(x => x * 2);
console.log(array1);
console.log(map1);
Outputs:
Array [undefined, undefined]
Array [NaN, undefined]
When the array is printed each of its elements are interrogated. The first has been assigned undefined the other is defaulted to undefined.
The mapping operation calls the mapping operation for the first element because it has been defined (through assignment). It does not call the mapping operation for the second argument, and simply passes out undefined.
As pointed out by #CertainPerformance, your array doesn't have any properties besides its length, you can verify that with this line: new Array(1).hasOwnProperty(0), which returns false.
Looking at 15.4.4.19 Array.prototype.map you can see, at 7.3, there's a check whether a key exists in the array.
1..6. [...]
7. 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
Let
kValue be the result of calling the [[Get]] internal
method of O with argument Pk.
Let
mappedValue be the result of calling the [[Call]] internal
method of callbackfn with T as the this
value and argument list containing kValue, k, and
O.
Call
the [[DefineOwnProperty]] internal method of A with
arguments Pk, Property Descriptor {[[Value]]: mappedValue,
[[Writable]]: true, [[Enumerable]]: true,
[[Configurable]]: true}, and false.
Increase
k by 1.
9. Return A.
As pointed out already, Array(2) will only create an array of two empty slots which cannot be mapped over.
As far as some and every are concerned, Array(2) is indeed an empty array:
Array(2).some(() => 1 > 0); //=> false
Array(2).every(() => 1 < 0); //=> true
However this array of empty slots can somehow be "iterated" on:
[].join(','); //=> ''
Array(2).join(','); //=> ','
JSON.stringify([]) //=> '[]'
JSON.stringify(Array(2)) //=> '[null,null]'
So we can take that knowledge to come up with interesting and somewhat ironic ways to use ES5 array functions on such "empty" arrays.
You've come up with that one yourself:
[...Array(2)].map(foo);
A variation of a suggestion from #charlietfl:
Array(2).fill().map(foo);
Personally I'd recommend this one:
Array.from(Array(2)).map(foo);
A bit old school:
Array.apply(null, Array(2)).map(foo);
This one is quite verbose:
Array(2).join(',').split(',').map(foo);

Unregistered member of array in length property JS

I came across this code that is used to keep both forward and reverse reference in an array:
var arr = [];
arr[arr['A'] = 0] = 'A';
arr[arr['B'] = 1] = 'B';
// On node interpreter
arr // [ 'A', 'B', A: 0, B: 1 ]
arr["A"] // 0
arr["B"] // 1
arr[0] // 'A'
arr[1] // 'B'
arr[2] // 'undefined'
arr.length // 2
The A: 0, B: 1 members get pushed to the end of the array.
What are these members and what happened in the process so that .length property recorded 2 instead of 4?
Storing a value with a string key into an array does not actually modify the array. It only adds a dynamic field to the Array object, unlike storing with a numeric index, which actually pushes a value into the array.. Array.length only reflects the number of elements in the array, as managed by the array, but not the number of dynamic fields in the array.
var arr = [];
arr["A"] = 2;
Here you are adding a property to the array object which does not reflect over the number of elements in the array.In javascript, elements of array are always stored using indices.
Array.length always returns the number of elements stored in the array.

Maintaining array order in Javascript

I am new to JavaScript and I am having trouble working with my array, I want my array ordered how I explicitly write it and not how JavaScript decides it wants it.
If we have a array
var array = {
0: 'zero',
4: 'four',
2: 'two'
};
When I choose to display this in the console, or iterate over it, Its reordered like this
array = {
0: 'zero',
2: 'two',
4: 'four'
};
I have tried 2 loops so far, The for loop, and also the for loop with the in statement.
Both work according how I assumed they would as they use a key and work there way up/down, making order I specify is absolutely useless.
How can I write/print/work with my array as its ordered, In other languages such as PHP its as simple as
$array = array(
0 => 'zero',
4 => 'four',
2 => 'two'
);
foreach($array as $key => $value)
echo $key ."\n";
This would output
0
4
2
Thanks in advance.
You're using an object {}, not an array []. Objects have no explicit order, where Arrays do. That's why you're having your problem. Change your {} to [] and you'll be fine. You could even use an array of objects.
var array = [
{0: 'zero'},
{4: 'four'},
{2: 'two'}
];
Looping over that like so
for(var i = 0; i < array.length; i++){
console.log(array[i]);
}
Gives us our normal order.
Object {0: "zero"}
Object {4: "four"}
Object {2: "two"}
Another Edit: The problem is you're trying to have an array that has properties and values just like an object, without using an object, {property: value} - that can't really be done with an array. To loop over an array like you want, it's as simple as
var arr = [1,2,3]
and
for(var i = 0; i < arr.length; i++){
console.log(i) //1,2,3
}
but the problem, like mentioned above, is you want more complex arrays which you simply can't do.
You need to understand what an Array and what an Object are, they are fundamentally different things and do not behave the same.
Array
Arrays are list-like objects whose prototype has methods to perform traversal and mutation operations. Neither the length of a JavaScript array nor the types of its elements are fixed. Since an array's size length grow or shrink at any time, JavaScript arrays are not guaranteed to be dense. In general, these are convenient characteristics; but if these features are not desirable for your particular use, you might consider using typed arrays.
Examples of an Array, note the magical length property, the values assigned to an Array are always found at their indexed locations; index is from 0 to 2^32 - 1
// dense arrays
var array1 = [1, 2, 3];
array1.length === 3;
array1[0] === 1;
var array2 = [];
array2[0] = 1;
array2[1] = 2;
array2[2] = 3;
array2.length === 3;
array1[1] === 2;
var array3 = [];
array3.push(1);
array3.push(2);
array3.push(3);
array3.length === 3;
array3[2] === 3;
// and a sparse array
var array4 = [];
array4[0] = 1;
array4[2] = 2;
array4[4] = 3;
array4.length === 5;
array4[1] === undefined;
Iterating an array for
var array = [1, 2, 3, 4, 5];
for (var index = 0; index < array.length; index += 1) { // counts from (index) 0 to 4
console.log(index, array[index]); // outputs 0 1 \n 1 2 \n 2 3 \n 3 4 \n 4 5
}
Object
An object is a collection of properties, and a property is an association between a name and a value. A property's value can be a function, in which case the property is known as a method. In addition to objects that are predefined in the browser, you can define your own objects.
Examples of an Object, there is no magical length property.
var object1 = {
'zero': 1,
'one': 2,
'two': 3
};
object1.zero === 1;
var object2 = {);
object2['zero'] = 1;
object2['one'] = 2;
object2['two'] = 3;
object2.one === 2;
var object3 = {);
object3.zero = 1;
object3.one = 2;
object3.two = 3;
object['two'] === 3;
Iterating (enumerating) an object for..in
var object = {
one: 0,
two: 1,
three: 2
};
for (var property in object) {
if (object.hasOwnProperty(property)) { // make sure the property belongs to object
console.log(property, object[property]); // outputs (not necessarily this order)
// three 2 \n two 1 \n one 0
}
};
If you are trying to maintain an ordered Object, then this is not how Javascript works and iteration (enumeration) of an object is arbitrary. There are techniques that you can use to iterate (enumerate) an Object in a known order. This requires that you keep an ordered list of properties in an array and use the array in the iteration process.
var object = {
'c': 1,
'z': 2,
'b': 3,
'a': 4
}
var propertyOrder = ['c', 'z', 'b', 'a'];
for (var index = 0; index < propertyOrder.length; index += 1) {
console.log(index, propertyOrder[index], object[propertyOrder[index]]); // outputs 0 c 1 \n 1 z 2 \n 2 b 3 \n 3 a 4
}
I don't get it. What makes you think that 0, then 4, then 2, is in any form or shape "in order"?
However, apparently what you want is to keep the number and the string together, in the order that you specify.
Your mistake, if you'll excuse me for perhaps sounding a bit harsh, is that you seem to think that you can use indices or member names to be both a means to access data and part of the data itself.
You can't, and you shouldn't. An index is an index, a name is a name, and data is data [1].
Here are two implementations of keeping your numbers and strings together:
var pairs = [
[0, "zero"],
[4, "four"],
[2, "two"]
];
for (var index in pairs) {
alert(pairs[index][0] + ": " + pairs[index][1]);
};
For this one, I keep the pairs of numbers and strings together in arrays of their own: the numbers at position 0, the strings at position 1. I store these arrays in pairs.
Then I iterate over array with for/in, which, in each iteration, gives me an index of my pairs array.
I then use that index to access the right sub-array in pairs, and get the number from position 0 and the string from position 1.
var pairs = [
{number: 0, string: "zero"},
{number: 4, string: "four"},
{number: 2, string: "two"}
];
for (var index in pairs) {
alert(pairs[index].number + ": " + pairs[index].string);
};
This one works exactly the same, except of storing the pairs in arrays, I store them in objects.
This has the added bonus of better readability inside the for loop: pairs[index][0] and pairs[index][1] do not really convey a clear meaning. It tells us nothing more than, "of the pair with the given index, get items 0 and 1".
However, pairs[index].number and pairs[index].name are much clearer: "of the pair with the given index, give me the number and the name."
_
Footnotes:
[1a] A number is a number: my bank account number is just that: a means to identify my bank account. It doesn't convey any meaning as to my name, my PIN, or the balance on my account -- all attributes that would qualify as data belonging to my bank account.
[1b] A name is a name: my name is just a name. When you're talking about me, you can use my name to refer to me, so that others know whom you are talking about. My name does not convey any information about my hobbies, my SSN, or what brand of car I have -- all attributes that would qualify as data that would describe certain aspects of me. (However, by convention, you can often tell a person's gender by their name)

Arrays created with new? [duplicate]

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.

Creating range in JavaScript - strange syntax

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

Categories