8

I'm just wondering if anyone knows why $.map and $.fn.map pass arguments in flipped order from one-another. Is there a valid reason (e.g. ECMA specs somewhere) for it or was it just a poorly-planned API that is now impossible to fix due to the amount of code relying on jQuery?

$.map([ 'a', 'b', 'c' ], function(){ console.log(arguments); })
// ['a', 0], ['b', 1], ['c', 2]

$.fn.map.call([ 'a', 'b', 'c' ], function(){ console.log(arguments); })
// [0, 'a'], [1, 'b'], [2, 'c']

.each doesn't act like this

Dharman
  • 30,962
  • 25
  • 85
  • 135
  • `$.fn.map.call` doesn't seem to be part of jQuery core. Am I missing something here? – Mrchief Jul 11 '11 at 22:25
  • @Mrchief - `$( ... ).map === $.fn.map`, I just wrote it different :) –  Jul 11 '11 at 22:26
  • No I meant `$.fn.map`, but never mind. My search wasn't right. – Mrchief Jul 11 '11 at 22:27
  • Sorry, `.call` is native JS. The 2nd statement is identical to `$([ 'a', 'b', 'c']).map(function(){ console.log(arguments); })` –  Jul 11 '11 at 22:31
  • To add to the confusion, ECMA 5's `.map()` function takes: the value of the element, the index of the element, and the Array object being traversed. https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map – Andrew Whitaker Jul 11 '11 at 22:46
  • Not an answer, but this is an excellent post about `$.map`, `$().map`, and `Array.prototype.map` http://www.mrspeaker.net/2011/04/27/reducing-map/ – Yahel Jul 11 '11 at 23:53
  • Possible duplicate of [Why are jQuery's callback arguments inconsistent?](http://stackoverflow.com/questions/3612320/why-are-jquerys-callback-arguments-inconsistent) – ryenus Sep 27 '16 at 12:56

3 Answers3

5

Indeed, that might be considered as an oversight, but there is a very good reason for the arguments to be passed in that order.

Of course, on the surface, that's because the map() / each() methods and $.map() are not supposed to be equivalent, and each side is meant to be used in its own way (dealing with jQuery objects and arrays/hashes, respectively).

But the main point is that the parameter position is optimized for the case your callback function only wants to take a single argument. Consider both cases:

  • map() and each() are meant to be called on jQuery objects, and the callback function will be called in the context of each DOM element involved in the mapping/loop (the context object is available through the this keyword). So, it makes more sense to pass the index first, then the element, because this already designates the element.

  • $.map() deals with arrays and Javascript objects (hashes), and the callback function is invoked in the "global" context (this is always the window object). So, it makes more sense to pass the value first, then the index, because mapping is about avoiding loop index variables in the first place. If the caller is interested in those, he can use a "standard" for loop (or the second argument passed to the callback function).

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
  • *"... and the callback function is invoked in the "global" context...So, it makes more sense to pass the value first..."* So why didn't they set the context of the `$.map()` callback to the current item like they do with `$.each()`? *"...because mapping is about avoiding loop index variables in the first place..."* So is `$.each()`. – user113716 Jul 11 '11 at 23:28
  • @Patrick, first, you're right, `each()` also avoids loop index variables, but it is purely iterative and doesn't return anything (that's why some functional folks would consider it a loop rather than a mapping, however, the same principle still stands). Now, as of why `$.map()` doesn't call back in the current value's context, I can only guess it might be confusing (and impractical) to be called with `this` referring to, say, a `String` or a `Number`. – Frédéric Hamidi Jul 11 '11 at 23:47
  • Yes, but `$.each()` does set `this` to a `String` or a `Number`. In any case, I think your answer does a very nice job of giving jQuery the benefit of the doubt. I tend to think that it just wasn't planned out as well as we would like. – user113716 Jul 11 '11 at 23:51
  • @Patrick, yup, that or we're both tired: documentation for [$.each()](http://api.jquery.com/jQuery.each/) says `the value can also be accessed through the this keyword, but Javascript will always wrap the this value as an Object even if it is a simple string or number value`. So it seems there are boxing considerations to take into account too: accessing the argument might be faster than `this` in that case. – Frédéric Hamidi Jul 11 '11 at 23:55
  • Yes indeed. Anyway +1 for taking the *"glass half full"* approach. :o) I would have been happier to see consistency between `$.each/$.map` and `.each()/.map()`. But minor points for methods that are overall easy to comprehend. – user113716 Jul 12 '11 at 01:03
3

Here's my completely and entirely speculative "answer".

jQuery.each(), .each() and jQuery.map() were all present in version 1.

.map() was added in 1.2.

My guess is that they simply didn't give consideration to having the each methods follow the forEach in the spec. (What was the status of ES 5 at that time anyway?) But for some reason they did follow closer to the spec for $.map(). (Maybe its original intention was for primarily internal use as well.)

So when jQuery 1.2 came along, they decided it would be nice to have a .map() method.

So the question they faced was to have .map() follow $.each() and .each(), or follow $.map().

Since .each() in particular had already been implemented as callable against a jQuery object, they felt that it would cause less confusion to have .map() follow .each() than to follow $.map(), which is most certainly not used as much.

Looking at the source for the .map() method vs the .each() method, you can see that .map() requires an intermediate function in order to flip around the arguments and set the this value. Somehow I doubt that they would have wanted to do that if they had it all planned out in advance.

Again, this is complete and utter speculation, but I wouldn't be surprised if it happened something like that.

Take it or leave it. ;o)

user113716
  • 318,772
  • 63
  • 451
  • 440
  • +1 for `take it or leave it`, although I still think the end result is superior to its potential alternatives. That would be natural selection, or maybe intelligent design, depending on your beliefs :] – Frédéric Hamidi Jul 12 '11 at 00:05
  • 1
    The most likely solution is probably the correct one. I consder this to be the most likely solution :) –  Jul 12 '11 at 00:36
-2

Ok, I did some digging in the code and the API changelog.

$.fn.map internally calls jQuery.map.

Its a guess (somewhat calculated) but I believe it was done to add the ability to traverse objects/collections.

Mrchief
  • 75,126
  • 20
  • 142
  • 189
  • 1
    I know what `.map` does, I'm wondering why the API calls are different. –  Jul 11 '11 at 22:39
  • Well it seems, the first one gives you an array back, the other one gives you a stack. – Mrchief Jul 11 '11 at 22:43
  • @Mrchief: `$.map` returns an Array and `$("...").map()` returns a jQuery object, as outlined in the documentation. The question has more to do with the order of the arguments that are passed to the callback function you can supply to each function. – Andrew Whitaker Jul 11 '11 at 22:44
  • Again, I know what they do :) The callbacks in each one are called with arguments in a different order. I'm wondering why: `$.map( [...], function( obj, index ){` vs `$.fn.map.call( [...], function( index, obj ){` <-- notice how "obj" and "index" are flipped? It's a question of ___why___ that is, not what it's doing. Basically "what were they thinking?" –  Jul 11 '11 at 22:45