Muffinresearch Labs by Stuart Colville

getElementsByClassName Deluxe Edition | 40 Comments

Posted in Code on 29th April 2006, 6:26 pm by

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

Post Tools

  • http://www.xaprb.com Xaprb

    These things can be taken to extremes, but if you wanted to use every optimization known to man, ++i is faster than i++. It avoids evaluating and just increments. My guess is, if you ran a hundred trillion loops with this optimization, you could see as much as a single nanosecond of speed increase ;-)

    Seriously: good work. What I like about these generically useful scripts is not so much that people are doing them, but that they’re exploring lots of angles on the same problem. If I need to use such a function, I have a rich variety of code to choose from, making it more likely I can find one that suits my purpose exactly.

  • http://muffinresearch.co.uk Stuart Colville

    That’s an interesting point about the iterator, might just implement that if it gives me the edge in terms of speed :-) .

    I like what you’re saying about different views on the same problem, I think that’s spot on.

  • http://www.reallyshiny.com Jack Sleight

    Hey, nice function. Incase your interested, i’m currently working on a function index for all the different getelementsby… functions. Ive added yours: http://www.getelementsby.com

  • http://nefariousdesigns.co.uk Tim Huegdon

    Sorry to rain on your parade, but that’s not the difference between ++i and i++.

    If you use ++i, Javascript will increment and then return the variable. However, if you use i++, Javascript will return the variable and then increment it’s value. This can best be illustrated with the following example:

    var i = 6;
    x = i++;
    alert(x); // This will display "6" in an alert box, because i was incremented AFTER it was returned.
    
    var i = 6;
    y = ++i;
    alert(y); // This will display "7" in an alert box, because i was incremented BEFORE it was returned.
    

    Since this is the case, I’m not sure how much of a speed improvement you’d accomplish by using it Stupot!

    For reference, here’s the Mozilla Javascript 1.5 Reference Manual document on Operators

  • http://muffinresearch.co.uk Stuart Colville

    @Tim: I think that the what Xaprb is referring to is something that is known to be true in C++ that the pre-increment uses a couple less stack operations compared to the post-increment which first increments and then returns a temporary of the original value.

    As you rightly point out, in JavaScript the post-increment returns the original value before the increment takes place so this indicates that there shouldn’t be any difference in terms of speed of execution. However, if anyone knows any different then I’d be interested to know.

  • http://www.robertnyman.com Robert Nyman

    So, as soon as I’m number one on Google with getElementsByClassName, you have to challenge me! :-)

    Nice work, though! I like the idea and the thinking behind some decisions.

    The reason some parameters are mandatory in my function is for performance reasons as well. If the default is document and the wildcard selector for tags (*), then few people will go through the hassle of optimizing their scripts to just search within the most suitable element/for the most suitable tag.

    My fear is just that they will go for the:

    Oh, let’s search everywhere, just to make sure…

  • http://muffinresearch.co.uk Stuart Colville

    @Robert:

    you have to challenge me

    You’re right I’m after the top spot ;-)

    Nice work, though! I like the idea and the thinking behind some decisions.

    Thanks very much.

    The reason some parameters are mandatory in my function is for performance reasons as well

    I totally appreciate what you did with the arguments in your geElementsByClassName however I wanted to use defaults for ease of use.

    I can see that quite often you would want to search the entire document for something, so for me it’s logical to make the containing element a default. The tagname one less so, as that’s often going to be just links or something else. If enough people tell me I should lose the defaults then maybe I can be swayed :) .

    Hopefully if anyone uses my version this they won’t just run it without thinking; after all, if you go looking for a getElementsByClassName function one would hope that you know what you are doing!

  • http://nefariousdesigns.co.uk Tim Huegdon

    @Stuart: Oh, I see; my apologies to Xaprb then! :)

  • http://www.moddular.org Andrew Walker

    The results from some limited testing (yes, I was bored this afternoon ;) )

    Each test creates an array and fills it with 100000 1′s and was repeated 50 times for each browser. (mean and median execution times in milliseconds)

    for (var i = 0; i < 100000; ++i) { }

    Firefox 1.5
    Mean: 275.6
    Median: 273.5

    Opera 9
    Mean: 224.7
    Median: 218.5

    IE 6
    Mean: 440.6
    Median: 438

    for (var i = 0; i < 100000; i++) { }

    Firefox 1.5
    Mean: 277.5
    Median: 266

    Opera 9
    Mean: 221.9
    Median: 204

    IE 6
    Mean: 441
    Median: 438

    var i = 100000;
    while (i–) { }

    Firefox 1.5
    Mean: 265.3
    Median: 266

    Opera 9
    Mean: 205
    Median: 195

    IE 6
    Mean: 436
    Median: 438

    This is somewhat inconclusive, ++i seems marginally faster than i++ in IE and Firefox and marginally slower in Opera, though this could be accounted for by errors in my decidedly non-scientific testing ;-) The while does appear to be a little faster than the for loops regardless of browser.

    The only thing I can say for certain is that IE is really slow ;-)

  • http://www.robertnyman.com Robert Nyman

    Nah, I think your function is fine. Keep the defaults. I just wanted to bring attention to that eventual problem (although I’m not sure I agree one wants to look through the entire document most of the time…).

    Of course I also hope people know what they’re doing when using such a function… :-)

  • http://nefariousdesigns.co.uk/ Tim

    @Andrew: Wow; you really WERE bored!

    It’s my understanding that for loops are slower than while loops in most languages. This is due to the increased complexity – just think what it takes to set-up a for loop.

    In PHP, the speed of the loop structures are in the following order (fastest first):

    1. do {} while ()
    2. while
    3. foreach
    4. for

    I would assume that Javascript is similar although I’ve never tested to quite the extent you have! ;)

    It’s interesting that Opera appears to suffer opposite effects to IE and Firefox when varying incremental operators!

  • http://muffinresearch.co.uk Stuart Colville

    @Andrew: Nice work on the tests, do you have a link to the test page you created?

  • http://www.moddular.org Andrew Walker

    @Tim: You’re probably right about that. I have a hunch that looping down to zero may help the speed a tiny bit too. I should probably try looping up and down with both the for and while loops at some point to see if that makes a difference :)

    @Stuart: Sure, you can get it here – http://www.moddular.org/tests/js-loop.html uncomment whichever version of the function you want to run. The page will run the function 10 times (many more and I started hitting script timeouts)

  • http://www.valuesoft.net Summer

    actually, if you put the .length outside the loop it should also be faster since it doesn’t reevaluate the length everytime

    length = myObjColl.length;

    for (i = 0, j = length; i

  • http://muffinresearch.co.uk Stuart Colville

    @Summer: No that’s not the case. The first argument to the for loop is only evaluated at the beginning. See Tim’s post – Loopy Control Structures, which goes into more detail regarding how the for loops work.

  • Kevin

    Hello, I just found your little script. It was posted at http://www.getelementsby.com/. Very useful for html help files. I was looking for a way to format certain text with stuff, and also replace the text inside with a set text. Now I just set the format with css and then use a for loop on the returned array. Keeps the html souce nice and tidy. Ofcourse, I had to change to <a> because of IE, but still, great script!!!

  • http://dyersweb.com curtis

    I have to say I’ve never given this much thought to optimization; you guys are quite talented! ;)

    @Andrew: that was some pretty good objective testing. Actually, just recently, I ran a similar optimization test in PHP in response to a post on the forums I mod. While loops do indeed seem to generally be faster.

    @Stuart: I also wrote a getElementsByClassName function kind of spur of the moment as part of a DOM tutorial I wrote. It’s quite crude in comparison to your finely tuned and crafted function, with notable optimizations. I forgot about accounting for the space delimited classes when I wrote mine, so I’ll have to update that sometime.

    Good job! :D

  • Ethan

    Just a note to newbies that might wonder why it’s not working for them:

    Your javascript has to execute AFTER the page has rendered. One way to do this is to put the code into a function (called ‘init’, for example) and then run the function by using:

    – edit –

    window.onload = init;

    Alternatively you could use your choice of addevent function so you will be able to add more than one function to run when the page loads.

    (I added everything after ‘– edit –’ as it looks like it was stripped in your original comment – stuart)

  • http://www.moddular.org Andrew Walker

    If anyone is interested I’ve written up some slightly more extensive tests on performance of javascript loops.

    http://www.moddular.org/log/javascript-loops/

    while loops (in particular while loops that count down) do seem a fraction faster.

  • Jon

    The function:


    Array.prototype.push = function(value) {
    this[this.length] = value;
    }

    seems to break my use of Ajax.InPlaceEditor from the script.aculo.us library. When that function is in my file then no parameters are send via POST or GET to the php file that processes the inline edit form. If I comment out this function then everything works fine. I can’t explain this but that’s what I found. If you don’t care about IE5/Mac then you can remove this without a problem. If you aren’t using Ajax.InPlaceEditor then it might not matter at all :-)

  • Max Shirshin

    getElementsByClassName may not work in Opera 9 when called without the second and third arguments. The error happens on this string:
    var arrObjClass = objColl[i].className.split(' ');
    Opera says that it cannot convert undefined or null value to an object. Further investigation showed that objColl returned by Opera indeed may contain some undefined elements (why they appear I cannot even guess).

    The following line fixes the problem:
    if( objColl[i].tagName == undefined ) continue;
    when placed before this line of the original script:
    var arrObjClass = objColl[i].className.split(' ');

  • http://anotherblog.spaces.live.com Arrix

    To make it even better, make iteration variable local. I mean, use for (var i = 0… instead of for (i = 0…

  • http://muffinresearch.co.uk Stuart Colville

    @Arrix: Well spotted those have now been correctly var’d!

  • Dr Phil

    Does not work in Opera 9. sux

  • Dr Phil

    In order to make this script work on Opera 9 you need to add this line:

    if(typeof objColl[i].className == "undefined") continue;

    right below the line:

    for (i = 0, j = objColl.length; i

    Have a good day!

  • http://muffinresearch.co.uk Stuart Colville

    @Max Shirshin: Thanks for you comment. I’ve finally gotten round to fixing the function for Opera 9 by excluding it from using the document.all method. In Opera 9 this returns textnodes as well as what it is supposed to, which puts a large spanner in the works. I’ve excluded it by testing for window.opera.

    @Dr. Phil: Your first comment was not the most constructive, but I’m glad you looked into it and found a fix. However as I have explained above, the better fix is to prevent Opera 9 using document.all.

  • Dan

    If speed is your primary concern, it seems kinda silly testing the value of delim, which never changes, so often.

    Why not test it once and have a seperate set of search loops for each of the three cases? The function will be longer, but execution will be faster (especially when only looking for a single class) and I suspect it’ll be more readable.

  • Eric Shepherd

    I just found a problem, after using this script for 6 months!

    If you don’t have a tag specified, but you do have a containing element specified, IE screws up because of the var objColl = declaration. It ends up referencing document.all incorrectly, rather than getting elements from the containing element. Adding another && to the if condition of the ternary operator fixes the problem. In my implementation, this line now reads:

    var objColl = (strTag == '*' && document.all && !window.opera && objContElm == document) ? document.all : objContElm.getElementsByTagName(strTag);

    Great script, btw. I’m glad this was a simple fix; let me know if there was a better way to solve the problem.

  • http://muffinresearch.co.uk Stuart Colville

    @Eric: I’ve made a simpler fix that should solve the problem plus it negates the need for the Opera check (see the comments above) by checking for getElementsByTagName returning something useful first. This is better as it should be available more often than not. This then just leaves document.all as a fallback.

  • Gavy

    Just want to say thanks for the example given. From your idea i have re-write my own function and that speeded up our application load time by almost 4s in a large dom! Great work!

  • Gary

    I like your script but because of one thing it’s not suitable for my purpose.

    var cont = document.getElementById('container');
    var myObjColl = getElementsByClassName('one two', 'a', cont);
    for (var i = 0, j = myObjColl.length; i 

    The argument ‘one two’ let you search for elements of class ‘one’ AND ‘two’ but I wanna search on a class name that has spaces in it.
    My suggestion it to use something like ‘one&two’ as what you did in the “OR” case.

  • http://muffinresearch.co.uk Stuart Colville

    @Gary: Unfortunately classes are always delimited by spaces so if you have a classname with a space in it you are going to run into problems across the board. To that end, I would suggest revising renaming that class to something more suitable.

  • javascriptn00b

    Looks really cool,

    anyone know if it is possible to create something that does
    document.getElementsByClass(class).style.display =’none’;

    basically the same as when using
    document.getElementsById(id).style.display =’none’;

    it would be a great help, if this would be possible

    Thx in advance lotz

  • http://muffinresearch.co.uk Stuart Colville

    @javasciptn00b: getElementsByClassname returns a collection, so to do what you want you would have to iterate over the collection in a loop and set the style property of each object in turn

    
    var c = getElementsByClassName('blah');
    
    for(var i=0;j=c.length; i<j; i++){
      c[i].style.display = 'none';
    }
  • riptide

    I can’t understand I have the whole code in the head of my document but it dosent work.

    var myObjColl = getElementsByClassName(‘tt1′, ‘a’);
    for (var i = 0, j = myObjColl.length; i

  • http://www.webinventif.fr/ k-ny

    Great tools !

  • http://www.buzzandpeople.com/ brad

    Hi !

    Really great ! Thanks for this !

  • Lessan

    Thanks for the script – used it as a quick replacement for this function from the protoype library, in an old site. Works much faster now.

  • Nick

    Nicely done!

    In the innermost loop, on line 17 of the function, you could change “c == arrClass.length” to “c == n”, since you already set n to the length of arrClass in the for statement.

    In fact, you could simplify the compound if statement on line 17 by pre-computing the second operand of the comparison before the outermost loop (two lines added; three changed):


    var arrClass = strClass.split(delim);
    var n = arrClass.length;
    var d = (delim == '|') ? 1 : n;
    for (var i = 0, j = objColl.length; i arrObjClass.length) continue;
    var c = 0;
    comparisonLoop:
    for (var k = 0, l = arrObjClass.length; k < l; k++) {
    for (var m = 0; m < n; m++) {
    if (arrClass[m] == arrObjClass[k]) c++;
    if (c == d) {
    arr.push(objColl[i]);
    break comparisonLoop;
    }
    }
    }
    }

    Also, if the user of getElementsByClassName accidentally puts more than one space between class names, as in…

    var myObjColl = getElementsByClassName('one two');

    … arrClass will contain a zero-length string, and the function will return an array with lots of extraneous, empty elements! Of course, you could argue that the user shouldn’t put more than one space between class names, but it wouldn’t be too difficult to make the function allow for this.

  • Boo

    Thanks for this function, it has now replaced the old one I was using which wasn’t working properly in IE6.

Insert a tab character in vim when expand tabs is on|(0)

I have vim set-up to use spaces in place of tabs. Sometimes you need to use an actual tab e.g. editing a Makefile. Now whilst it’s possible to change settings so that tabs are used for specific files, a quick tip to remember is to simply type in insert mode:

Ctrl+v tab

That is Ctrl and “V” and hit the tab key, et voila you’ve entered an actual tab.

GNU screen: open tab in current working directory|(1)

A nice trick for having screen open a new tab in the same directory as the one you’re currently in. To use it add it to your .screenrc

# Open new window in current dir.
bind c stuff "screen -X chdir \$PWD;screen^M"
bind ^c stuff "screen -X chdir \$PWD;screen^M"

Hat tip: mteckert on SuperUser.com

Photos on Flickr

© Copyright 2004-13 Stuart Colville, all rights reserved. May contain traces of Muffin. Powered by WordPress. Hosting by Slicehost.com This page was baked in 0.566s.