Tuesday, July 10, 2012

jQuery.each()

jQuery.each() has many forms and functions, and the documentation is lacking. Here's what the documentation has to say about it:

http://api.jquery.com/each/

.each(arrayfunction(index, Element))

function(index, Element)   A function to execute for each matched element.

As it turns out this is a significant oversimplification of this function. It varies in 3 ways:
  1. Whether it's called on the global jQuery object like $.each(collectionfn), or on a jQuery instance like $('div').each(fn).
  2. When used in the global form, whether it's called on an array or an object.
  3. Whether it's called with any additional arguments.
Before I introduce any examples let's use a simple jsfiddle for testing these variations on .each():

The code is pretty simple. There's a Run button that can call a simple say(s) function that appends whatever HTML is passed in to the div id=output below the button. Now we can inspect the code, click Run, and see the results. The code I'll be demonstrating will always appear under the // Demo code comment.

$.each(array, fn)

Let's begin with the most common usage - calling the global version on an array:

var array ['hi''bye''hello?'];
$.each(arrayfunction(ival{
    say(': ' val);
});

This is called on the global jQuery object rather than an instance. The arguments passed to the function are the index in the array, and then the value at that index. One important subtlety here is that this has been coerced into also being the value at that index of the array. In other words this code can be simplified to:

var array ['hi''bye''hello?'];
$.each(arrayfunction(i{
    say(': ' this);
});

A minor variation, but the reduced number of params may minify slightly smaller. If you were only using the values in the array and not the indices, you could pass a function with no params whatsoever:

var array ['hi''bye''hello?'];
$.each(arrayfunction({
    say(this);
});

$('tag').each(fn)

Calling .each() on a jQuery instance object is identical to calling it on an array, except you pass the function as the first argument this time, and the subtlety of what this refers to has changed - it now refers to the tag in the list of tags jQuery has selected. Not a jQuery object wrapping that tag, but the actual browser-specific Element object. The value object passed earlier is now that same tag object.

$('span').each(function(itag{
    say(': ' tag.innerHTML);
});

This performs identically:
$('span').each(function(i{
    say(': ' this.innerHTML);
});

$.each(obj, fn)

Looping through an object varies significantly from the array format. The function you pass in is now of the format:

function(pv{
}

Where p is the property name (a string), and v is the value of that property in the object. For example:

var obj {
    a'hi',
    b'bye',
    c'hello?'
};
$.each(objfunction(pv{
    say(': ' v);
});

Here we build a simple object to iterate through, and print out its properties by name and value. Notice that there's no way to get an index from this function anymore. If you do want to keep track of how many items have been counted, you'll need to tack that on yourself via a closure:

var obj {
    a'hi',
    b'bye',
    c'hello?'
};
var 0;
$.each(objfunction(pv{
    say((i++',' ': ' v);
});

And as you might expect, this is the value of the property currently selected, meaning the following code is identical to /6:

var obj {
    a'hi',
    b'bye',
    c'hello?'
};
$.each(objfunction(p{
    say(': ' this);
});

Caveats of using this

jQuery's reuse of the this object in Javascript comes with some built-in dangers due to the Javascript language itself. Suppose you had an array of boolean values like:

var array [truefalsetrue];

And decided to loop over it checking whether they were true or false. What would you expect?

If you notice, the values printed out this time are identical - they all come out to false! Why? Because in Javascript converting the special variable this to boolean always converts it to true, even if the underlying object is itself boolean and set to false!

This is not an easy rule to remember, and jQuery's coercion of the value of this in $.each() encourages making this mistake. Given that the code savings of using this are limited, in any environment with multiple coders or even where you simply worry about making mistakes, it might be smart to forbid the use of the this keyword inside $.each() calls and always use the parameters passed to the function. This prevents both this misunderstanding, and people expecting some other object in the place of this (like the array being looped through, or what this refers to outside the closure created by this function).

Related Functions

You occasionally want to get a specific tag from a list of tags selected by jQuery rather than looping through the list. If you want that tag as its browser-specific Element object, you can access it like:

$('span')[2]

Which returns the 3rd tag in the list jQuery has selected.

If you rather have the jQuery-wrapped tag, you use the strangely-named .eq method instead:

$('span').eq(2)

Alternative to Plain Loop

It's common to use $.each() in place of a standard for loop:

for(var 0array.lengthi++{
    say(': ' array[i]);
}

$.each(arrayfunction(ival{
    say(': ' val);
});

The code savings here are most significant when you use the this form and aren't using the index in the loop:

for(var 0array.lengthi++{
    say(array[i]);
}

$.each(arrayfunction({
    say(this);
});

The problem with using $.each() in place of a regular for loop is that you can no longer use break to break out of the loop early. For this you must use an alternative syntax - return false:

for(var 0array.lengthi++{
    say(array[i]);
    if (array[i== 'bye')
        break;
}

$.each(arrayfunction({
    say(this);
    if (this == 'bye')
        return false;
});

But, there's still one thing a regular for loop can do that $.each() can't, and that's return from the entire function early - for example if you're searching an array for a specific value:

function indexOf(arrayval{
    for(var array.length 1>= 0i--{
        if (array[i== val)
            return i;
    }
    return i;
}

Unfortunately there isn't really an equivalent to this in $.each() - at best you can inject a flag into the closure and check it to decide whether to return early:


Here's the same example where the loop is allowed to complete:


$.each(array, fn, args)

jQuery maintains a number of APIs it uses internally but asks that you don't use externally. So, if it saves you some code, there's no harm in leveraging them, so long as you write a unit test that validates that internal API still works, and check it whenever you update jQuery.

With that warning, the internal version of $.each() is used to call a function with a fixed set of arguments on all the items in an array. Here's a silly example:

var array ['hi''bye''hello?'];
$.each(arraysayItem['monkeys']);

You can probably infer how jQuery makes use of this - any time it needs to loop through a series of tags or objects and set the same property on all of them, it can use this shorthand. You can too:

function setBgColor(color{
    this.style.backgroundColor color;
}
$('p').each(setBgColor['#f00']);

This can also be useful if your function is externally defined, and you want to pass it some external context information (rather than creating a closure). Consider these 2 examples of building an object from an array:


In the latter example, enableFlag is defined outside our code, which would have required creating an extra closure around it to pass in the hashset we want to toggle these flags on. We can avoid a little code bloat with this usage. You can leverage this a bit farther:


The Code

You can see the above cases pretty easily in the main code for $.each() (this is taken from jQuery 1.7.2).

// args is for internal usage only
eachfunctionobjectcallbackargs {
    var name0,
        length object.length,
        isObj length === undefined || jQuery.isFunctionobject );

    if args {
        if isObj {
            for name in object {
                if callback.applyobjectname ]args === false {
                    break;
                }
            }
        else {
            for length{
                if callback.applyobjecti++ ]args === false {
                    break;
                }
            }
        }

    // A special, fast, case for the most common use of each
    else {
        if isObj {
            for name in object {
                if callback.callobjectname ]nameobjectname === false {
                    break;
                }
            }
        else {
            for length{
                if callback.callobject]iobjecti++ === false {
                    break;
                }
            }
        }
    }

    return object;
},

No comments:

Post a Comment