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?
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?
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.
In the first line, the array constructor is called as a function with
Function.prototype.apply
.this
value isnull
which does not matter for the Array constructor (this
is the samethis
as in the context according to 15.3.4.3.2.a.new Array
is called being passed an object with alength
property - that causes that object to be an array like for all it matters to.apply
because of the following clause in.apply
:.apply
is passing arguments from 0 to.length
, since calling[[Get]]
on{ length: 5 }
with the values 0 to 4 yieldsundefined
the array constructor is called with five arguments whose value isundefined
(getting an undeclared property of an object).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})
andArray(5)
, the first creating five times the primitive value typeundefined
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:
Now off to the second part.
Array.prototype.map
calls the callback function (in this caseNumber.call
) on each element of the array and uses the specifiedthis
value (in this case setting thethis
value to `Number).Number.call
) is the index, and the first is the this value.Number
is called withthis
asundefined
(the array value) and the index as the parameter. So it's basically the same as mapping eachundefined
to its array index (since callingNumber
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:
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:if we pass the function foo as the first argument it will be called for each element with
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 offoo
, it would bethis
.Two examples:
and another one just to make it clearer:
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 functionbaz
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
Understanding this "hack" requires understanding several things:
Array(5).map(...)
Function.prototype.apply
handles argumentsArray
handles multiple argumentsNumber
function handles argumentsFunction.prototype.call
doesThey'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 regularkey => value
map, just like any other object. Let's play with arrays a little, shall we?We get to the inherent difference between the number of items in the array,
arr.length
, and the number ofkey=>value
mappings the array has, which can be different thanarr.length
.Expanding the array via
arr.length
does not create any newkey=>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 getundefined
.Now we can lift our heads a little, and see why functions like
arr.map
don't walk over these properties. Ifarr[3]
was merely undefined, and the key existed, all these array functions would just go over it like any other value: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: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:(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 definelen
items on the array, we don't create thekey => value
mappings, we simply alter thelength
property.Now that we have that out of the way, let's look at the second magical thing:
2. How
Function.prototype.apply
worksWhat
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:Now, we can ease the process of seeing how
apply
works by simply logging thearguments
special variable:It's easy to prove my claim in the second-to-last example:
(yes, pun intended). The
key => value
mapping may not have existed in the array we passed over toapply
, but it certainly exists in thearguments
variable. It's the same reason the last example works: The keys do not exist on the object we pass, but they do exist inarguments
.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:Which basically means:
argArray.length
. The spec then proceeds to do a simplefor
loop overlength
items, making alist
of corresponding values (list
is some internal voodoo, but it's basically an array). In terms of very, very loose code:So all we need to mimic an
argArray
in this case is an object with alength
property. And now we can see why the values are undefined, but the keys aren't, onarguments
: We create thekey=>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:
3. How
Array
handles multiple argumentsSo! We saw what happens when you pass a
length
argument toArray
, but in the expression, we pass several things as arguments (an array of 5undefined
, 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: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:
We saw that it returns an array containing 5 undefined values, with keys all in existence.
Now, to the second part of the expression:
This will be the easier, non-convoluted part, as it doesn't so much rely on obscure hacks.
4. How
Number
treats inputDoing
Number(something)
(section 15.7.1) convertssomething
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
isapply
'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:This is quite wtf worthy until you grasp what's going on.
log.call
is just a function, equivalent to any other function'scall
method, and as such, has acall
method on itself as well:And what does
call
do? It accepts athisArg
and a bunch of arguments, and calls its parent function. We can define it viaapply
(again, very loose code, won't work):Let's track how this goes down:
The later part, or the
.map
of it allIt's not over yet. Let's see what happens when you supply a function to most array methods:
If we don't provide a
this
argument ourselves, it defaults towindow
. 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: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:So, we get this:
Now we can see how
.map(Number.call, Number)
works:Which returns the transformation of
i
, the current index, to a number.In conclusion,
The expression
Works in two parts:
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: