In JavaScript: The Good Parts, Douglas Crockford explains that JavaScript, like most languages, contains a mix of good parts and bad ones. Logically, he identifies that superior code adopts the language’s strengths and avoids its pitfalls. This post borrows that premise and applies it to jQuery, one of the most popular JavaScript libraries. jQuery contains a lot of useful functionality, but it also contains some dangers when written without scrutiny or deeper understanding.

Using a presentation I gave at our Spring offsite as a foundation1, I will explore the good parts of jQuery that we use at Yext and provide some explaination as to why they are so good.

jQuery is still JavaScript

Because jQuery is so easy to use, some new programmers start with jQuery and fail to learn fundamental JavaScript. jQuery’s excellent documentation and extensive support on Stack Overflow readily give the how but often skim over the why. However, understanding native JavaScript leads to better design choices, more effective debugging, and improved performance.

As a library, jQuery is written in plain JavaScript, as its source code betrays. So, if you’re ever worried about the internals of a function you’re using, just check out the source code yourself.

DOM traversal isn’t free

When I started using jQuery, I used selector queries like they were variable names. I wrongfully assumed there was no cost associated with each query, because I didn’t understand that each call, in fact, queried the entire DOM to find the selector. There isn’t a way to get around traversing the DOM to find an element, but there are ways we can make that traversal less costly.

Reduce traversal size with $.fn.find

Since we must check every element in a collection to see if it matches a selector, we may decrease that cost by reducing the collection’s size. Luckily, jQuery gives us the method $.fn.find, which allows us to traverse only the children of an element. This is helpful for two reasons. First, it reduces the number elements we need to look through. Second, it prevents us from including elements we don’t want when using generic selectors.

// searches whole DOM
var $dialog = $('.js-dialog');

// searches only children
var $textArea = $dialog.find('.js-text-input');

// prevents selecting unwanted elements
var $actionBtn = $dialog.find('.action');

Cache elements as variables

The quickest way to access an element is to not search for it at all. We can do this by assigning elements we have already searched for as variables.

// search for .js-div twice per loop
for (var i = 0; i < $('.js-div').data('limit'); i++) {
  $('.js-div').append('<span>');
}

// search for .js-div once
var $div = $('.js-div');
var limit = $div.data('limit');
for (var i = 0; i < limit; i++) {
  $div.append('<span>');
}

We can extend this principle when writing class-like objects. If we define all the object’s relevant elements in the constructor, we gain several benefits. First, we see all the important elements in one place. Second, we only need to query them once. Third, we would only need to update a selector in once place if we make any changes to it.

var YextDialog = function() {
  this.$dialog = $('.js-yext-dialog');
  this.$form = $dialog.find('form');
  this.$confirmBtn = $dialog.find('.confirm-btn');
  this.$closeBtn = $dialog.find('.close-btn');

  this.$confirmBtn.on('click', function() { this.submit(); });
  this.$closeBtn.on('click', function() { this.close(); });
}

YextDialog.prototype.submit = function() {
  this.$form.submit();
}

YextDialog.prototype.close = function() {
  this.$dialog.hide();
}

Use better selectors

Unlike English, jQuery reads its selectors from right to left. That means that when you execute $('.content p'), jQuery first creates a list of all the p elements, then goes through that list and searches for an ancestors with .content. We can prevent extra searches by selecting for a specific selector, usually a class.

var $bad    = $('.locations tr');         // searches right to left
var $better = $('.locations').find('tr'); // parent then children
var $best   = $('.js-locations-row');     // single selector

Leverage event delegation

Clicking a button in the browser also clicks on its container and every other one of its ancestors. This concept is called event propagation, which is comprised of two phases: capturing and bubbling. You can read about the specifics in another article, but for now, the important take away is that events affect entire element lineages.

Event delegation takes advantage of event propagation by asking a delegate to handle events for its children. Thus, elements that handle events identically can instead ask a common ancestor to listen for the event and handle it when one of the specified children is targeted.

Choose event delegation over repetition

A common example of event delegation is binding a single event to a list instead of a separate event to each list item. By passing in additional selector into a jQuery event listener, the parent will execute the handler on the event target if it matches the provided selector.

// an event listener for each .js-li
$('.js-li').on('click', function() {
  console.log('li click');
});

// just one event listener on .js-ul
$('.js-ul').on('click', '.js-li', function() {
  console.log('li click');
});

Pair event delegation with AJAX

Another advantage of event delegation is that it doesn’t need children to exist when it is bound. This is useful when loading elements via AJAX, because you can bind an event handler before the AJAX call occurs. Then, when a child is targeted by an event, its parent will correctly delegate a handler to it.

var $table = $('.js-table');

// event handler before content is loaded
$table.on('click', '.js-delete-row', function() {
  $(this).closest('tr').hide(); // this = element matching .js-delete-row
});

$.ajax({
  url: 'page/populateTable',
  dataType: 'html',
  success: function(data) {
    $table.html(data);
  }
});

Utilize helpful methods

jQuery is full of other useful methods that prevent developers from reinventing the wheel. I recommend skimming through the jQuery documentation, but in the meantime, here are some of the most useful.

Simplify AJAX calls

In addition to an extremely simple interface, jQuery’s $.ajax accepts several callbacks that will execute at various points during an AJAX request. In the following example, these callbacks manage a loading icon displayed during the AJAX call and actions taken on either success or error.

$.ajax({
  url: '/listings/edit',          // string URL
  data: { locationIds: [1,2,3] }, // data object, converted to str
  dataType: 'json',               // reponse type: text, html
  type: 'GET',                    // GET or POST
  beforeSend: function() {        // called before request sent
    $dialog.loadingStart();
  },
  success: function(data) {       // called if request succeeds
    $dialog.displaySuccess(data);
  },
  error: function(data) {         // called if request fails
    $dialog.displayError(data);
  },
  complete: function() {          // called after response received
    $dialog.loadingEnd();
  }
});

Copy and construct objects with $.extend

Copying objects in JavaScript can be tricky, since a change to an object will be reflected in every reference to that object. jQuery provides a simple method that enables copying and combining objects. This can be extremely useful when instantiating new objects, since you can combine user-defined options with default options without affecting either for future instantiations.

var Car = function(opts) {
  var options = $.extend(
    {},
    Car.defaults,
    opts
  );
  this.initialize(options);
}

Car.defaults = {
  color: 'red',
  wheels: 4
};

var car = new Car({ color: 'yellow'}); // { color: 'yellow', wheels: 4 }

Access data attributes with $.fn.data

Data attributes are extremely helpful, and their power is further heightened in jQuery. In addition to strings, jQuery allows us to easily store any type as a data attribute. The native JS property dataset gives similar functionality, but older browsers like IE8 do not have access to the native property.

In this example, we store an object as a data attribute containing some basic information about the element. If we wanted, we could even store a function.

var $elements = $('.js-elements');

$elements.each(function(i) {
  $(this).data('elementData', {
    index: i + 1,
    total: $elements.length
  });
});

var $ele = $elements.get(0);
var data = $ele.data('elementData');

console.log($ele + ' is element ' + data.index + ' out of ' + data.total);

Be stylish

Good algorithms improve performance, but good style improves organization, legibility, and maintainability. Unlike browser performance, however, there isn’t a good way to measure style. What is a good style for one person may be poor one for someone else. These are style conventions we use at Yext, which I find to be very stylish anywhere.

Prepend jQuery variables with $

When adopting jQuery into a codebase, a common problem is the mixing of native JavaScript and jQuery. jQuery elements do not have access to native properties and vice versa. Thus, prepending identifiers for jQuery objects with a dollar sign (commonly associated with jQuery) clearly indicates which elements are jQuery objects and which are native DOM elements.

// does this take a jQuery or native element?
function setColor(ele, color) {
  ele.dataset.color = color; // will fail if jQuery
  ele.data('color') = color; // will fail if native
}

// much clearer
function setColor2($ele, color) {
  $ele.data('color') = color;
}

Prepend JS selectors with js-

At Yext, we also prepend any selector used to find an element in JavaScript with js-. This allows us to distinguish which selectors will break functionality if changed versus which are tied only to styles. This way, if someone removes a class while refactoring styles, they can be sure it will not affect the JavaScript.

// prepend with js-
var $css = $('.form-submit-btn');    // CSS selector
var $js  = $('.js-form-submit-btn'); // JS selector

The Good Parts

The good parts of jQuery have been invaluable to me and to Yext over the years. Fortunately, through better understanding of JavaScript and jQuery, we’ve been able to minimize some of head- and browser-aches. Learning to dive into the benefits and drawbacks of jQuery has helped me do the same for other libraries and languages, and I hope you will join me in learning both sides of every coin (and then weighting that coin so that it always lands good side up).


  1. The picture of a presenter wearing fun pants is me!