jQuery Best Practices Part 1: Managing Scope

I've been asked by a large number of developers if I know any solid jQuery best practices. The answer is a huge YES! I'm also getting around to changing my answer to their immediate follow-up. Do you have them written down so you can share them with me? In an effort to change my answer to another huge YES, I am starting a series on jQuery Best Practices that will continue until I run out of steam. The first best practice that I have to share is how to properly manage the scope of your jQuery scripts.

The Problem of Scope

Every developer I have talked to uses the following standard $(document).ready() invocation:

$(document).ready(function()
    $('.button').click(function(){
        alert("You Clicked The Button!");
    });
});

This invocation assumes one very significant behavior: You will only be altering your DOM at page load...ever. I cannot stress enough how nasty of an assumption this is. In almost any modern web application, the DOM is going to be changing after the page load. I see countless developers encounter this when they first start looking at AJAX interactions using jQuery. Their first reaction is almost always the same: run the $(document).ready() again...and hilarity ensues. Not the good kind of hilarity...but the kind where it honestly feels like the browser itself is laughing at you.

The next step that many people take is to execute the necessary jQuery as part of the AJAX success handler like so:

$.ajax({
    type: 'POST',
    url: 'http://mysite.com/ajaxurl',
    success: function(data) {
        $('.data-target').html(data);
        $('.data-target .button').click(function(){
            alert("You Clicked The Button!");
        });
    }
});

Everything is now behaving as expected. Unfortunately this behavior has come at the cost of a blatant DRY(Don't Repeat Yourself) violation.

How To Manage Scope

How I manage the scope of my jQuery scripts is founded on the following jQuery statements being equivalent:

$('.button').click(function(){
    alert("You Clicked The Button!");
});
$(document).find('.button').click(function(){
    alert("You Clicked The Button!");
});

In the first, I added the click handler in the standard manner. In the second, I searched for the button class in the document. These form the basis for my best practices because the second allows you to control the scope of the DOM that is being altered. You can explicitly focus the scope of your jQuery by wrapping your script in the a function:

function my_button_jquery(scope){
    scope.find('.button').click(function(){
        alert("You Clicked The Button!");
    });
}

Given this function, we can quickly rewrite the document ready from the first example script like so:

$(document).ready(function()
    my_button_jquery($(this));
});

And rewrite the ajax call in a similar manner:

$.ajax({
    type: 'POST',
    url: 'http://mysite.com/ajaxurl',
    success: function(data) {
        $('.data-target').html(data);
        my_button_jquery($('.data-target'));
    }
});

If you are one of the dozen people or so that ready my post on jQuery and ASP UpdatePanel Integration you will recognize what I am doing here.

When to Break Scope

Now that we have additional control over the primary scope of our jQuery script, we need to understand where things are going to break. The scope variable will only be reliable within the my_button_jquery function. You must assume that this scope will be broken any time you enter into an anonymous function. The reason for this is the asynchronous nature of the JavaScript event model.

This is why the above ajax call is not written with a scope inside the success handler:

    success: function(data) {
        scope.find('.ajax-form-wrapper').html(data);
        my_button_jquery(scope.find('.ajax-form-wrapper'));
    }

Notes on Efficiency

You should always invoke the jQuery object outside of your function to improve efficiency:

$(document).ready(function () {
    my_button_jquery($(this));
});

Not:

$(document).ready(function () {
    my_button_jquery(document);
});

Invoking a variable through jQuery is an expensive task that should only be performed once. Many other bloggers have mentioned storing the result of $(this) in a variable to avoid repeated and unnecessary calls to the jQuery object. I'm not a strict follower of that pracitice—because I am more than willing to knowingly introduce inefficiencies into executing to improve maintainability. Instead I go for the low-hanging fruit such as invoking jQuery outside of my scoped initializer.

The other area where scoping can be inefficient is in the number of jQuery selections that will not perform any action. Consolidating all of you script into a scoped initializer means that they will all be executed if an AJAX call updates the DOM. I will cover this in my next post—which will cover chaining and flow control.

Attachments:
February 9th, 2012