Accueil / Writing JavaScript without anonymous functions

Writing JavaScript without anonymous functions

Most of the JavaScript you see on the web is full of anonymous functions. I don't think most of them are necessary. Allow me to demonstrate.

Imagine a PHP developer—let's call her Sunny—who doesn't really write JavaScript, but needs to toggle some text on a page. Sunny goes searching for examples on Google, and finds this jQuery snippet:

Click to hide

Bacon ipsum dolor sit amet venison ham hock pork loin, labore sunt fatback id. Bresaola short ribs ball tip tail, quis ea ribeye. Andouille velit tail, minim shankle non hamburger consectetur jowl biltong. Short ribs shank pancetta, excepteur corned beef ut eiusmod labore kielbasa.

Sunny doesn't really get what's going on with the nested function () {} business, but the code works fine. So she happily sticks it into a <script type="text/javascript"></script>, changes the HTML ids to match her content, and forgets all about it... until the next month, when her boss says, "Hey, remember that great little show/hide feature you wrote? Marketing wants the same thing for every section on the page."

Now Sunny has to figure out how to make that little snippet reusable. "No problem!" she thinks. "I'll just make it into a function that takes a couple of id parameters so it knows what links are toggling which paragraphs." So she gives that a try:

function toggle(link_id, paragraph_id) {
  var $link = $('#' + link_id);
  var $paragraph = $('#' + paragraph_id);
  // Show the paragraph if it's hidden.
  if ($paragraph.is(':hidden')) {
    $paragraph.show();
    $link.text('Click to hide');
  }
  // Hide the paragraph if it's already showing.
  else {
    $paragraph.hide();
    $link.text('Click to show');
  }
}

She tests it out in the Firebug console:
toggle('toggler1', 'paragraph1');
It works great! The link text changes and the paragraph toggles. "Now all I have to do is wire it up to the click event," thinks Sunny. "I'll get paragraph1 working, and then I'll iterate over the rest of them." So off she goes:

$('a#toggler1').click(toggle('toggler1', 'paragraph1'));

If you've written much JavaScript, you know what's going to happen when she clicks on that link: Exactly nothing. Eventually Sunny figures out her problem: The jQuery click() function, which you use to add a click event handler, takes a function argument. You can't pass it toggle('toggler1', 'paragraph1'). You have to pass the toggle function itself. JavaScript treats functions as first-class objects, and you pass them around just like any other object.

"But I can't pass just toggle—that doesn't have enough information! How am I supposed to pass in the parameters I need?" Sunny protests. She Googles around some more, and she finds out about the magic of closures! As it turns out, if you write a JavaScript function inside another function, the inner function can see all the variables in the outer function. "So that's why you see all these function () {} declarations inside other functions! Maybe I should turn toggler back into one of those." So she tries again:

Click to hide

Veggies sunt bona vobis, proinde vos postulo esse magis burdock groundnut salad bell pepper fennel turnip greens cabbage desert raisin caulie squash. Burdock horseradish epazote asparagus broccoli taro avocado kohlrabi wattle seed sweet pepper water spinach arugula bunya nuts grape.

Turns out that works fine. But there are still a couple of problems with it, one of which Sunny knows about, and one of which she has yet to discover:

  1. This is going to quit working in a very odd way when she tries iterating over it to add the other paragraphs.
  2. When Sunny went from having a nice, clean, encapsulated toggle function to an anonymous inline function, her code suddenly became harder to read, harder to document, and harder to maintain.

I won't expound on problem #1; there are several ways to solve it (including wrapping her code in yet another (function(){})(), oh joy!). What I want to deal with here is problem #2:

Anonymous inline functions like the one in the second example are hard to read, hard to document, and hard to maintain.

Often developers like Sunny use them because they don't see any other way to get to the data they need. But there's a perfectly good alternative pattern.

Step 1: Use an object

Just like in other programming languages, objects in JavaScript are a good way to keep data and methods together. Sunny can write her object constructor like this:

Toggler = function (linkId, paragraphId) {
  this.$link = $('#' + linkId);
  this.$paragraph = $('#' + paragraphId);
};

To add methods, you add them to the object's prototype:

Toggler.prototype.toggle = function () {
    if (this.$paragraph.is(':hidden')) {
      this.$paragraph.show();
      this.$link.text('Click to hide');
    }
    else {
      this.$paragraph.hide();
      this.$link.text('Click to show');
    }
  });
};

When Sunny wants a new instance of Toggler, she can instantiate it like this:

var toggler1 = new Toggler('toggle-link-1', 'paragraph-1');

Step 2: Function.prototype.bind (or jQuery.proxy)

So now we have an object, which is fine and dandy, but we still have the same problem Sunny had when she tried passing toggle('toggle-link', 'paragraph'): How do we turn toggler1.toggle into something we can pass into jQuery's click() method? The answer is Function.prototype.bind. It's a new JavaScript feature (introduced in ECMAScript 5) that lets you package up a method on an object instance, together with the instance itself, and turn the whole thing into a first-class function you can use as an event handler. Here's how it works:

var toggler1 = new Toggler('toggle-link-1', 'paragraph-1');
var click_handler = toggler1.toggle.bind(toggler1); // <-- this is the magic invocation!
$('a#toggle-link-1').click(click_handler);

That .bind() call is the key to this whole pattern. It's a method on the built-in Function object's prototype. Its superpower is that it lets Sunny specify what she wants this to equal when her method gets called. The this keyword is always supposed to refer to the object that your function is a method of—just like it does in Java or PHP or any number of other languages—but in JavaScript, it doesn't always work that way. For example, if you create a click handler without using .bind() on it, you'll find that this is a reference to the link that got clicked! That's pretty crazy behavior for anyone familiar with object-oriented programming. Fortunately, by using .bind() on your object methods, you can keep the meaning of this sane.

One thing to be aware of is that older browsers don't have Function.prototype.bind available natively. This includes IE8, FF3.6, and all versions of Safari. So for those browsers, you have to provide your own definition of Function.prototype.bind. Mozilla has a handy polyfill you can use. It won't hurt anything in modern browsers, and it'll make .bind() work correctly in older ones.

Alternatively, if you have jQuery 1.4 or higher available, you can use jQuery.proxy in much the same way:

var toggler1 = new Toggler('toggle-link-1', 'paragraph-1');
var click_handler = $.proxy(toggler1.toggle, toggler1);
$('a#toggle-link-1').click(click_handler);

I don't really recommend jQuery.proxy in most cases. It tries to keep track of your bound functions for you, so you can unbind them later even without a reference to your function object, and that can cause problems if you're creating multiple event handlers (say, one for each toggle link on the page, which is Sunny's use case). I do often use it when writing JavaScript for Drupal, though, since Drupal doesn't have Function.prototype.bind available (yet!).

Putting it together

Here's how Sunny could write her toggler code using objects and Function.prototype.bind:

Click to hide

Skateboard quis et vegan cliche lomo messenger bag, mcsweeney's mollit lo-fi american apparel sint tofu blog. Etsy wolf PBR irony nesciunt, scenester ut mollit freegan cosby sweater mixtape viral vero. Tofu culpa freegan magna, non aute bicycle rights dolor gentrify DIY esse.

Click to hide

Sockeye salmon. Saber-toothed blenny flagfish banded killifish dragonfish goldeye boga fathead sculpin sargassum fish. Unicorn fish sandperch zebra bullhead shark Atlantic saury. Panga goby, “darter tripletail,” barreleye walleye rockweed gunnel southern flounder.

Obviously this isn't the most efficient way to implement a toggle link—there are lots of better ways to do it (jQuery toggle(), for one!). The point here is to illustrate how .bind() lets you write object-oriented JavaScript that respects the this keyword. It's a technique that could eliminate a lot of undocumented anonymous functions if more people knew about it.

I'm especially interested in getting more people in the Drupal community familiar with this pattern, so if you maintain a Drupal module and you want to give this a try, come find me on IRC in #drupal-contribute and I'll be happy to help. I've also proposed a workshop at the Pacific Northwest Drupal Summit where we'll be refactoring JavaScript from the audience. If you're coming to the summit and you have some JavaScript you're not proud of, bring it by for a few small repairs!

Comments are closed; visit the original article at http://jsdemystified.drupalgardens.com/content/only-you-can-prevent-anonymous-functions to comment.