Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>Understanding this "hack" requires understanding several things:</p> <ol> <li>Why we don't just do <code>Array(5).map(...)</code></li> <li>How <code>Function.prototype.apply</code> handles arguments</li> <li>How <code>Array</code> handles multiple arguments</li> <li>How the <code>Number</code> function handles arguments</li> <li>What <code>Function.prototype.call</code> does</li> </ol> <p>They're rather advanced topics in javascript, so this will be more-than-rather long. We'll start from the top. Buckle up!</p> <h2>1. Why not just <code>Array(5).map</code>?</h2> <p>What's an array, really? A regular object, containing integer keys, which map to values. It has other special features, for instance the magical <code>length</code> variable, but at it's core, it's a regular <code>key =&gt; value</code> map, just like any other object. Let's play with arrays a little, shall we?</p> <pre><code>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'] </code></pre> <p>We get to the inherent difference between the number of items in the array, <code>arr.length</code>, and the number of <code>key=&gt;value</code> mappings the array has, which can be different than <code>arr.length</code>.</p> <p>Expanding the array via <code>arr.length</code> <em>does not</em> create any new <code>key=&gt;value</code> mappings, so it's not that the array has undefined values, it <em>does not have these keys</em>. And what happens when you try to access a non-existent property? You get <code>undefined</code>.</p> <p>Now we can lift our heads a little, and see why functions like <code>arr.map</code> don't walk over these properties. If <code>arr[3]</code> was merely undefined, and the key existed, all these array functions would just go over it like any other value:</p> <pre><code>//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"] </code></pre> <p>I intentionally used a method call to further prove the point that the key itself was never there: Calling <code>undefined.toUpperCase</code> would have raised an error, but it didn't. To prove <em>that</em>:</p> <pre><code>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 </code></pre> <p>And now we get to my point: How <code>Array(N)</code> does things. <a href="http://es5.github.io/#x15.4.2.2" rel="noreferrer">Section 15.4.2.2</a> 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:</p> <pre><code>function Array(len) { var ret = []; ret.length = len; return ret; } </code></pre> <p>(operates under the assumption (which is checked in the actual spec) that <code>len</code> is a valid uint32, and not just any number of value)</p> <p>So now you can see why doing <code>Array(5).map(...)</code> wouldn't work - we don't define <code>len</code> items on the array, we don't create the <code>key =&gt; value</code> mappings, we simply alter the <code>length</code> property.</p> <p>Now that we have that out of the way, let's look at the second magical thing:</p> <h2>2. How <code>Function.prototype.apply</code> works</h2> <p>What <code>apply</code> 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:</p> <pre><code>function foo (a, b, c) { return a + b + c; } foo(0, 1, 2); //3 foo.apply(null, [0, 1, 2]); //3 </code></pre> <p>Now, we can ease the process of seeing how <code>apply</code> works by simply logging the <code>arguments</code> special variable:</p> <pre><code>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] </code></pre> <p>It's easy to prove my claim in the second-to-last example:</p> <pre><code>function ahaExclamationMark () { console.log(arguments.length); console.log(arguments.hasOwnProperty(0)); } ahaExclamationMark.apply(null, Array(2)); //2, true </code></pre> <p>(yes, pun intended). The <code>key =&gt; value</code> mapping may not have existed in the array we passed over to <code>apply</code>, but it certainly exists in the <code>arguments</code> 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 <code>arguments</code>.</p> <p>Why is that? Let's look at <a href="http://es5.github.io/#x15.3.4.3" rel="noreferrer">Section 15.3.4.3</a>, where <code>Function.prototype.apply</code> is defined. Mostly things we don't care about, but here's the interesting portion:</p> <blockquote> <ol start="4"> <li>Let len be the result of calling the [[Get]] internal method of argArray with argument "length".</li> </ol> </blockquote> <p>Which basically means: <code>argArray.length</code>. The spec then proceeds to do a simple <code>for</code> loop over <code>length</code> items, making a <code>list</code> of corresponding values (<code>list</code> is some internal voodoo, but it's basically an array). In terms of very, very loose code:</p> <pre><code>Function.prototype.apply = function (thisArg, argArray) { var len = argArray.length, argList = []; for (var i = 0; i &lt; len; i += 1) { argList[i] = argArray[i]; } //yeah... superMagicalFunctionInvocation(this, thisArg, argList); }; </code></pre> <p>So all we need to mimic an <code>argArray</code> in this case is an object with a <code>length</code> property. And now we can see why the values are undefined, but the keys aren't, on <code>arguments</code>: We create the <code>key=&gt;value</code> mappings.</p> <p>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:</p> <pre><code>Array.apply(null, { length: 5 }).map(Number.call, Number); </code></pre> <h2>3. How <code>Array</code> handles multiple arguments</h2> <p>So! We saw what happens when you pass a <code>length</code> argument to <code>Array</code>, but in the expression, we pass several things as arguments (an array of 5 <code>undefined</code>, to be exact). <a href="http://es5.github.io/#x15.4.2.1" rel="noreferrer">Section 15.4.2.1</a> tells us what to do. The last paragraph is all that matters to us, and it's worded <em>really</em> oddly, but it kind of boils down to:</p> <pre><code>function Array () { var ret = []; ret.length = arguments.length; for (var i = 0; i &lt; 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] </code></pre> <p>Tada! We get an array of several undefined values, and we return an array of these undefined values.</p> <h2>The first part of the expression</h2> <p>Finally, we can decipher the following:</p> <pre><code>Array.apply(null, { length: 5 }) </code></pre> <p>We saw that it returns an array containing 5 undefined values, with keys all in existence.</p> <p>Now, to the second part of the expression:</p> <pre><code>[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number) </code></pre> <p>This will be the easier, non-convoluted part, as it doesn't so much rely on obscure hacks.</p> <h2>4. How <code>Number</code> treats input</h2> <p>Doing <code>Number(something)</code> (<a href="http://es5.github.io/#x15.7.1.1" rel="noreferrer">section 15.7.1</a>) converts <code>something</code> 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 <a href="http://es5.github.io/#x9.3" rel="noreferrer">section 9.3</a> in case you're interested.</p> <h2>5. Games of <code>Function.prototype.call</code></h2> <p><code>call</code> is <code>apply</code>'s brother, defined in <a href="http://es5.github.io/#x15.3.4.4" rel="noreferrer">section 15.3.4.4</a>. Instead of taking an array of arguments, it just takes the arguments it received, and passes them forward.</p> <p>Things get interesting when you chain more than one <code>call</code> together, crank the weird up to 11:</p> <pre><code>function log () { console.log(this, arguments); } log.call.call(log, {a:4}, {a:5}); //{a:4}, [{a:5}] //^---^ ^-----^ // this arguments </code></pre> <p>This is quite wtf worthy until you grasp what's going on. <code>log.call</code> is just a function, equivalent to any other function's <code>call</code> method, and as such, has a <code>call</code> method on itself as well:</p> <pre><code>log.call === log.call.call; //true log.call === Function.call; //true </code></pre> <p>And what does <code>call</code> do? It accepts a <code>thisArg</code> and a bunch of arguments, and calls its parent function. We can define it via <code>apply</code> (again, very loose code, won't work):</p> <pre><code>Function.prototype.call = function (thisArg) { var args = arguments.slice(1); //I wish that'd work return this.apply(thisArg, args); }; </code></pre> <p>Let's track how this goes down:</p> <pre><code>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}]) </code></pre> <h2>The later part, or the <code>.map</code> of it all</h2> <p>It's not over yet. Let's see what happens when you supply a function to most array methods:</p> <pre><code>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 </code></pre> <p>If we don't provide a <code>this</code> argument ourselves, it defaults to <code>window</code>. 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:</p> <pre><code>arr.forEach(log.call, log); //'a', [0, ['a', 'b', 'c']] //'b', [1, ['a', 'b', 'c']] //'b', [2, ['a', 'b', 'c']] // ^ ^ </code></pre> <p>Whoa whoa whoa...let's back up a bit. What's going on here? We can see in <a href="http://es5.github.io/#x15.4.4.18" rel="noreferrer">section 15.4.4.18</a>, where <code>forEach</code> is defined, the following pretty much happens:</p> <pre><code>var callback = log.call, thisArg = log; for (var i = 0; i &lt; arr.length; i += 1) { callback.call(thisArg, arr[i], i, arr); } </code></pre> <p>So, we get this:</p> <pre><code>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); </code></pre> <p>Now we can see how <code>.map(Number.call, Number)</code> works:</p> <pre><code>Number.call.call(Number, arr[i], i, arr); Number.call(arr[i], i, arr); Number(i, arr); </code></pre> <p>Which returns the transformation of <code>i</code>, the current index, to a number.</p> <h2>In conclusion,</h2> <p>The expression</p> <pre><code>Array.apply(null, { length: 5 }).map(Number.call, Number); </code></pre> <p>Works in two parts:</p> <pre><code>var arr = Array.apply(null, { length: 5 }); //1 arr.map(Number.call, Number); //2 </code></pre> <p>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:</p> <pre><code>[0, 1, 2, 3, 4] </code></pre>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload