jQuery.map() in 1.6

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.

[js] var myObj = { "name": "jordan", "hair_color": "brown", "eye_color": "ravishing" }; [/js]

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

[js] var objKeys = []; $.each( myObj, function( key, value ) { objKeys.push( key ); }); // objKeys == [ "name", "hair_color", "eye_color" ] [/js]

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

[js] var objKeys = $.map( myObj, function( value, key ) { return key; }); // objKeys == [ "name", "hair_color", "eye_color" ] [/js]

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:

[js] length = object.length, isObj = length === undefined || jQuery.isFunction( object ); [/js]

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:

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

I should note that John Resig added:

[js] || length === 0 [/js]

To support empty node lists.

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

[js] elems instanceof jQuery [/js]

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.

[js] $('div').map( function( i, element ) { // under the hood, this call to map passes the // isArray variable check right away because // $('div') is an instance of jQuery // using 'this' here refers to the DOM element }); [/js]

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

[js] $.map( $('div'), function( element, i ) { // does the same as above and will pass the "instanceof" check // also, note that the arguments are backwards // using 'this' here refers to the window object }); [/js]

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:

[js] $.map( [1, 3, 5] , function(){} ); [/js]

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.

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

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!