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;
},

Wednesday, January 18, 2012

With the Internet, We're All Street Performers

There are many articles online that echo the MPAA talking points about PIPA and SOPA, the Senate and House versions of a law that gives media companies the right to command law enforcement to go after those they target with little or no judicial review, no requirement of evidence, no right of the victims to challenge their accusers.

For me, these articles all do away with any authority they could have brought to the issue by attempting to associate those who download MP3s and MP4s as "thieves." The thieves they try to remind you of are those breaking into someone's home, or taking something off your person. Some articles go even farther and try to compare downloading to drug trade, or even terrorism.

Let's be clear in our terminology. Downloading an MP3 is a form of "theft," but not that kind. In order to take something from you or your house, I need to deny you access to it. In order to download an MP3, I take nothing - I enjoy your efforts without paying you, like someone passing a street performer without putting anything in the hat.

There are no laws stating you owe a street performer, even if you stick around while they play. But media companies don't see themselves this way either because they're full of themselves, or they see the end of that legal road, and don't want to go there.

To be concrete, Louis CK, Trent Reznor, Radiohead, and several more artists have posted their work for free or variations (sometimes a small flat price, sometimes you pay what you want etc), and made plenty of money anyway. I haven't heard them outwardly acknowledge they're doing a global street performance, but the business model is identical: Some paid. Some didn't. The artist did well. At its core this is how media works - you put it out there and you hope some people like it enough to pay for it. Some like it and don't pay. If you're unlucky, few like it in the first place.

Implicit in this exchange are the ideas of availability and value. On value, the consumers who pay do so in part because they value the work and want to do their part to see more like it made in the future. The consumers who don't may have many reasons, but at least part of that reasoning is that they view the cost as too high for the work.

On availability, media companies are stuck in a circular argument in which they've managed to jail themselves. Media companies apply region protection to their distributions. They'll release a movie first in the US, then take it to DVD and on those DVDs they'll apply a Region Code that locks those DVDs to the US. Then they'll license that movie to Netflix and in that licensing contract, lock distribution to only US. Later, if there was enough revenue in the US, they might target global markets.

During the time between initial release and global release there are international customers, many who would likely pay for it if they could. The internet has no region codes, no licensing restrictions - and so many download it for free, in part because it's their only option. In return, media companies increase their accusations and fear of international customers and increase their attempts at restricting regional distribution in a confused desire to keep their content from reaching international shores.

I'll pause for those skeptical - you might point out that media companies must do things this way for a reason, and there's no proof it would get better if they got rid of these cave-tech region restrictions. But remember that the artists doing their own releases above did not region-restrict their media, and had numerous international customers. For a larger example, I point to a company that has made a business model out of global release, a video game distributor: Valve. Valve created a platform called Steam they use as a global marketplace to sell games. While the Steam platform warrants a lengthy analysis of its own, let's stick to the one point for now: Global availability. Valve has seen piracy drop dramatically in migrating to a global release model. Don't take my word for it - here's founder Gabe Newell on how this change has made some unexpected markets like Russia some of their best places for sales.

Inside the US, international availability is a minor issue (some travel enough to not want the irritation of a region-restricted DVD), but in many cases convenience is the issue instead - in this case the availability issue is their living room, not their country. I can go on a torrent site right now and download every movie and TV show I've ever heard of. I can go on Netflix, Hulu, Amazon, etc and get a randomly changing slim slice of that same selection. Shouldn't I start where the selection is greatest?

And frankly in many cases the value just isn't there. Big movie companies think they should be getting $10 at the box office - now that they're adding the 3D gimmick, they think they should be getting $15-20. Millions of downloaders do so in part because they disagree.

We don't need new laws that force those consumers into compliance with what these content distributors feel they're owed, and force consumers to wait until they have the privilege of availability. Both of these are attempts at perverting the market. If Congress really wants guidance on how to write a new law, begin by doing away with the worst parts of the DMCA, where for example free speech can be squashed by making a false copyright claim. Then, if more protections are needed - and perhaps they are - look at Valve and Louis CK's business model, and ask them what would help them, not Viacom and Rupert Murdoch.

We all hope to create content at some point in our lives that others enjoy, and hopefully even pay for. But the big businesses are trying to force us back into an old model and aren't ready to accept that in this new global market place where media moves so readily, we're all street performers. Even the big guys.