This tip sports the following code:
Array.apply(null, {length: N}).map(Function.call, Number);
This is classic High Magic to most developers. It fuses familiar objects and methods in strange combinations, and you get the impression that if you deviate from this incantion by just the tiniest bit, the world will end in a nuclear fireball. You therefore use it Precisely As Written, and pray that you're never asked to explain it, or fix it if it fails.
But there's no reason to invoke the Elder Gods, when we can pull back the magic curtain and understand...
We begin with a simple object, {length: 3}
. It's not an array:
Object.prototype.toString.call([1,2,3])
=> "[object Array]"
Object.prototype.toString.call({length: 3})
=> "[object Object]"
but it is an array-like object: it has a numeric length
property, and we can access it with the relevant numeric indexes:
a = {length: 3} // -> Object {length: 3}
a[0] // -> undefined
a[1] // -> undefined
a[2] // -> undefined
(Those undefined
s are actually irrelevant; all that's important here is that JS returned a value even when you tried to access a non-existent "index".)
This comes in handy because, starting with ES5, Array.apply()
accepts array-like objects and gets its length
property to iterate over the contents of that object, in a classic for (i = 0; i < args_obj.length; i++)
loop.
Since the internal algorithms for .call()
and .apply()
differ only in how the arguments are treated, we can declare the following:
X.apply(obj, arr )
=> X.call (obj, arr[0], arr[1], ...)
=> X ( arr[0], arr[1], ...) // this = obj
which resolves our second step into an Array
constructor call as follows:
b = Array.apply(null, a )
=> b = Array.call (null, undefined(a[0]), undefined(a[1]), undefined(a[2]))
=> b = Array ( undefined, undefined, undefined ) // this = null
=> b = [undefined, undefined, undefined ]
We can verify that it's a proper array, and that it supports .map()
for our next step:
Object.prototype.toString.call(b) // -> "[object Array]"
Object.keys(b) // -> ["0", "1", "2"]
b.length // -> 3
b.map // -> function map() { [native code] }
The logic of b.map(fn, obj)
is essentially as follows (see this polyfill implementation for the gory details):
result = new Array(b.length);
for (var i = 0; i < b.length; i++) {
result[i] = fn.call(obj, b[i], i, b);
}
return result;
But that means .map(Function.call, Number)
iterates over the following:
Function.call.call(Number, b[i], i, b)
=> Function.call ( b[i], i, b) // this = Number
=> Number ( i, b) // this = b[i]
=> i // Number(x, ...) = x as a number
(At this point, I'll replace the definition of b
with a slightly different 3-element array:
b = ["Tom", "Dick", "Harry"]
I promise that it won't affect the end result; it'll just make the next illustration much clearer.)
So b.map(Function.call, Number)
resolves as follows:
["Tom", "Dick", "Harry"].map(Function.call, Number)
=> [
Function.call.call(Number, "Tom", 0, ["Tom", "Dick", "Harry"]),
Function.call.call(Number, "Dick", 1, ["Tom", "Dick", "Harry"]),
Function.call.call(Number, "Harry", 2, ["Tom", "Dick", "Harry"])
]
=> [
Function.call ( "Tom", 0, ["Tom", "Dick", "Harry"]), // this = Number
Function.call ( "Dick", 1, ["Tom", "Dick", "Harry"]), // this = Number
Function.call ( "Harry", 2, ["Tom", "Dick", "Harry"]) // this = Number
]
=> [
Number ( 0, ["Tom", "Dick", "Harry"]), // this = "Tom"
Number ( 1, ["Tom", "Dick", "Harry"]), // this = "Dick"
Number ( 2, ["Tom", "Dick", "Harry"]) // this = "Harry"
]
=> [
0,
1,
2
]
QED.
Not very. It's true that most of Array.apply(null, {length: N}).map(Function.call, Number)
needs to be exactly as stated, but there are two terms that are actually quite flexible:
null
: Notice that this = null
appears too late in the resolution process to be of any use, so you could safely substitute literally any JS entity.
Function.call
: This can be replaced with any X.call
that inherits from Function.call
. In fact, almost everyone traditionally writes the expression as:
Array.apply(null, {length: N}).map(Number.call, Number)
as if Number.call
needed to be paired with Number
for the expression to evaluate correctly. The real "magic" is simply that Number.call
inherits from Function.call
, nothing more.
This means that all the following expressions return exactly the same result as the main expression:
function h() {
console.log("Hello, world!");
}
Array.apply(document, {length: 3}).map(SyntaxError.call, Number)
=> [0, 1, 2]
Array.apply(234, {length: 3}).map(Object.call, Number)
=> [0, 1, 2]
Array.apply(new Date("2040-01-01"), {length: 3}).map(h.call, Number)
=> [0, 1, 2]
// NOTE: "Hello, world!" was NOT printed, which is to be expected given the resolution process in Step 3.