jQuery.map() in 1.6

read 7 comments

Among all of the great fixes and additions to jQuery 1.6, I'm happy to say that jQuery.map() now supports objects! The previous map only supported arrays. With other libraries already offering object support for map, it was a nice addition.

Let's say you want to collect an array of object keys from a JSON object.

JavaScript:
  1. var myObj = {
  2.     "name": "jordan",
  3.     "hair_color": "brown",
  4.     "eye_color": "ravishing"
  5. };

Here's how you would have done it in older versions of jQuery (prior to 1.6):

JavaScript:
  1. var objKeys = [];
  2. $.each( myObj, function( key, value ) {
  3.     objKeys.push( key );
  4. });
  5. // objKeys == [ "name", "hair_color", "eye_color" ]

Here's the new way (just a little more convenient):

JavaScript:
  1. var objKeys = $.map( myObj, function( value, key ) {
  2.     return key;
  3. });
  4. // objKeys == [ "name", "hair_color", "eye_color" ]

Going inside the new jQuery.map()

For those that are curious and a little more advanced, there are some neat things going on under the hood of the new jQuery.map() that I would like to talk about.

Adding object support seems pretty trivial at first since jQuery.each() is already doing it - so it must be an easy patch, right? Well, not really. Let's look at how jQuery.each() is doing it. If you look at the jQuery source on github, you'll see that it's doing:

JavaScript:
  1. length = object.length,
  2. isObj = length === undefined || jQuery.isFunction( object );

Can you see the flaw in this? It's going to treat the variable object (could be an array or object) like an object if length is undefined. What happens when I have an object with a "length" property? It dies a horrible death. Some have reported this issue.

For the new jQuery.map(), we wanted support for objects and also be able to pass an object with a length property and not have it blow up like jQuery.each() does. Dan Heberden came to the rescue. Dan spent some time making sure that jQuery.map() didn't face the same fate while keeping performance in mind.

Here is what Dan did to see if elems is an array:

JavaScript:
  1. length = elems.length,
  2. isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length> 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;

I should note that John Resig added:

JavaScript:
  1. || length === 0

To support empty node lists.

Let's look at the first part of our isArray variable. You'll notice it does a check for:

JavaScript:
  1. elems instanceof jQuery

We're checking to see if elems is an instanceof jQuery. We're doing this first because you're most likely dealing with a jQuery collection/array-like object. Since jQuery collections are treated like regular arrays (uses for loop), this is an optimized way of checking if we should treat elems like an array. Below is an example of jQuery.fn.map() in action.

JavaScript:
  1. $('div').map( function( i, element ) {
  2.     // under the hood, this call to map passes the
  3.     // isArray variable check right away because
  4.     // $('div') is an instance of jQuery
  5.     // using 'this' here refers to the DOM element
  6. });

Here is jQuery.map() with a jQuery collection passed as the first argument:

JavaScript:
  1. $.map( $('div'), function( element, i ) {
  2.     // does the same as above and will pass the "instanceof" check
  3.     // also, note that the arguments are backwards
  4.     // using 'this' here refers to the window object
  5. });

If what you passed is not an instance of jQuery but it passed the isArray variable check, it means your call to the jQuery.map() utility function probably looked something like this:

JavaScript:
  1. $.map( [1, 3, 5] , function(){} );

In this case it falls back to this rigorous check to see if it's an array. Note: I'm breaking it down into separate lines so it's easier to understand.

JavaScript:
  1. // make sure that we actually have a length property
  2. length !== undefined
  3.     // if it's a number, it could possibly be an array
  4.     // but still needs some more checking
  5.     && typeof length === "number"
  6.     && (
  7.         // make sure we're dealing with a set of non-empties
  8.         ( length> 0 && elems[ 0 ] && elems[ length -1 ] )
  9.         // if we're dealing with an empty node list - this is rare
  10.         || length === 0
  11.         // this check is a last resort and only gets hit if we
  12.         // pass an array like $.map(new Array(1), fn);
  13.         || jQuery.isArray( elems )
  14.     )

The rest of the logic is pretty straight forward - just some for loops and super fun for-in loops.

Hope you enjoy jQuery.map() with object support!

comment feed

7 comments

  1. kuindji

    Well,

    $.map({length: 0}, function(v,k) {
    return k;
    });

    returns an empty array.

    • @kuindji: That is a known issue. Something that's being pondered over due to the fact that it adding additional logic would affect performance. It's a balancing act between performance and making sure the most common use-case is covered - which we're assuming it won't be common for people to pass length: 0 as an object.

  2. It seems that this code:

    // make sure we're dealing with a set of non-empties
    ( length> 0 && elems[ 0 ] && elems[ length -1 ] )

    doesn't actually do what it says that it's trying to do. [0,0] will make that part of the condition pass. Of course, it'll eventually drop into the jQuery.isArray() call. Perhaps a little better:

    ( length > 0 && ( typeof elems[ 0 ] !== undefined ) && ( typeof elems[ length -1 ] !== undefined) )

    but maybe that falls on the other side of the balance between additional logic and performance.

  3. Awesome! Didnt know JQuery could do this, thanks for the write up.... will be referencing back to it in the future

  4. Hi
    Even if you can achieve the same result with jQuery.each(), but having alternatives for doing something is not that bad. Though, consistency may decrease and developers may loose time learning that new things are not increasing their programming power, and just provide additional ways to do the same thing.

3 Pings

  1. jQuery.map() in 1.6 » Learning jQuery - Jordan Boesch...

    Thank you for submitting this cool story - Trackback from DotNetShoutout...

Sorry, but comments for this entry are now closed.