getElementsByClassName Deluxe Edition

Anyone who's into JavaScript will most likely have written their own take on a getElementsByClassName function. I had a look around at a few recent examples and then decided to do my own version for fun. Out of all of the functions I looked at Robert Nyman's came closest to what I wanted (kudos!) but my version has a few subtle differences up it's sleeve.

The final result works in WinIE, Firefox, Opera, Safari and IE Mac.

My original criteria was as follows:

  1. Be as fast as possible
  2. The order of arguments to be the most logical for ease of use
  3. Have optional arguments with defaults
  4. To support multiple classnames in any order

After showing my original function to Tim he thought that when finding multiple classnames I was looking for class1 OR class2 OR class3, whereas to start with I was looking for class1 AND class2 AND class3. Then it became obvious I should provide a means to do either, as being able to search in that way could come in very handy, way to go Timbo!

The Defaults

You may notice that I set my defaults up using what looks like a slightly odd syntax so I'll cover this in case you haven't come across this before:

strTag = strTag || "*"; 

What this does is uses the logical operator OR instead of an if statement. Written in this way if strTag evaluates to false then strTag will be set to '*'.

The optimised loops

Right from the beginning I decided to use loops instead of regex. This made it possible to do much more with simpler code whilst avoiding the 'expense' of regex. To make the for loops as efficient as possible I use the following syntax:

for (var i = 0, j = objColl.length; i < j; i++) 

instead of:

for (var i = 0; i < objColl.length; i++) 

The reason for this is that the second example queries the length of the array on every iteration which is inefficient.

The second important optimisation I used was to break out of the loops when the script has found all of the matches needed, thus cutting down on iterations. To do this I used a loop label which enabled the break statement to break out of two loops in one go. For more information on Loop labels Chris Heilmann knocked up this little post after I asked him about browser support for the loop label syntax. Incidentally this method works on every browser he threw at it and comes in very handy when you are looking to cut down on iterations.

Another line that helps to cut down on wasting iterations is the following line:

if (delim == ' ' && arrClass.length > arrObjClass.length) continue;

What this does is check if we are looking for several classes if the array generated from the class(es) string passed in has more classNames than the array of the current element objects classNames then there's no way a match can be made so this line means that we'll just move on to the next element instead.

Within the nested loop I am first iterating around the current object's classes and within the inner loop I am iterating around the array of passed in classes for comparison. Everytime I find a match a counter is incremented. If the OR method is being used then once the counter is equal to one the current element object is pushed on to the collection and the nested loop structure is broken out of. With the AND method I am checking on each inner iteration to see if the counter is equal to the length of the array generated from the class(es) string passed in. Once it does, again the the current element object is pushed onto the collection we are building and the nested loop is broken out of to move onto the next element.

The source


function getElementsByClassName(strClass, strTag, objContElm) {
  strTag = strTag || "*";
  objContElm = objContElm || document;                                                    
  var objColl = objContElm.getElementsByTagName(strTag);
  if (!objColl.length &&  strTag == "*" &&  objContElm.all) objColl = objContElm.all;
  var arr = new Array();                              
  var delim = strClass.indexOf('|') != -1  ? '|' : ' ';   
  var arrClass = strClass.split(delim);
  for (var i = 0, j = objColl.length; i < j; i++) {                         
    var arrObjClass = objColl[i].className.split(' ');   
    if (delim == ' ' && arrClass.length > arrObjClass.length) continue;
    var c = 0;
    comparisonLoop:
    for (var k = 0, l = arrObjClass.length; k < l; k++) {
      for (var m = 0, n = arrClass.length; m < n; m++) {
        if (arrClass[m] == arrObjClass[k]) c++;
        if (( delim == '|' && c == 1) || (delim == ' ' && c == arrClass.length)) {
          arr.push(objColl[i]); 
          break comparisonLoop;
        }
      }
    }
  }
  return arr; 
}

// To cover IE 5.0's lack of the push method
Array.prototype.push = function(value) {
  this[this.length] = value;                                                              
}

You can download the full script here getElementsByClassName.js (2k)

Usage

The function has three parameters:

strClass:
string containing the class(es) that you are looking for
strTag (optional, defaults to '*') :
An optional tag name to narrow the search to specific tags e.g. 'a' for links.
objContElm (optional, defaults to document)
An optional object container to search inside. Again this narrows the scope of the search

The following example will get all elements within the entire document that have a class of 'one'. This will involve a slower search as the criteria are less specific. Obviously if this is the only way you can do what you need that's ok but if you can narrow down the search criteria then the collection will be generated more quickly.

var myObjColl = getElementsByClassName('one');
for (var i = 0, j = myObjColl.length; i < j; i++) {
   // Do your thing here.
}

The next example will get all 'a' elements (links) that have a class of 'one' and are found within the cont object which in these examples is an element with the id 'container'.

var cont = document.getElementById('container');

var myObjColl = getElementsByClassName('one', 'a', cont);
for (var i = 0, j = myObjColl.length; i < j; i++) {
   // Do your thing here.
}

This next example will get all 'a' elements (links) that have a class of 'one' AND 'two' and are found within the cont object.

var cont = document.getElementById('container');

var myObjColl = getElementsByClassName('one two', 'a', cont);
for (var i = 0, j = myObjColl.length; i < j; i++) {
   // Do your thing here.
}

Finally this example will get all 'span' elements that have a class of 'one' OR 'two' and are found within the cont object.

var cont = document.getElementById('container');

var myObjColl = getElementsByClassName('one|two', 'span', cont);
for (var i = 0, j = myObjColl.length; i < j; i++) {
   // Do your thing here.
}

Demonstration

I have provided a fairly comprehensive test page so you can see this function in action.

Changelog

1.03
29-11-06 Changed the object collection to use the most common methods first.
1.02
07-08-06 Some variables in the for loops were missing the var keyword.
1.01
15-08-06 Fixed issue in Opera 9.0 where document.all returned textnodes.

References

comments powered by Disqus