Muffinresearch Labs by Stuart Colville

getElementsByClassName Deluxe Edition | Comments (40)

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

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

Comments: Add yours

1. On April 29th, 2006 at 8:12 pm Xaprb said:

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.

2. On April 30th, 2006 at 12:56 am Stuart Colville said:

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.

3. On April 30th, 2006 at 11:32 am Jack Sleight said:

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

4. On April 30th, 2006 at 5:31 pm Tim Huegdon said:

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

5. On May 1st, 2006 at 12:30 am Stuart Colville said:

@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.

6. On May 1st, 2006 at 12:31 pm Robert Nyman said:

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…

7. On May 1st, 2006 at 1:38 pm Stuart Colville said:

@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!

8. On May 1st, 2006 at 3:14 pm Tim Huegdon said:

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

9. On May 1st, 2006 at 6:59 pm Andrew Walker said:

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 ;-)

10. On May 1st, 2006 at 8:09 pm Robert Nyman said:

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… :-)

11. On May 2nd, 2006 at 9:51 am Tim said:

@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!

12. On May 2nd, 2006 at 1:12 pm Stuart Colville said:

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

13. On May 2nd, 2006 at 7:58 pm Andrew Walker said:

@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)

14. On May 4th, 2006 at 1:19 pm Summer said:

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

15. On May 4th, 2006 at 1:28 pm Stuart Colville said:

@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.

16. On May 5th, 2006 at 11:39 pm Kevin said:

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!!!

17. On May 14th, 2006 at 5:24 am curtis said:

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

18. On June 5th, 2006 at 2:04 am Ethan said:

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)

19. On June 5th, 2006 at 9:44 pm Andrew Walker said:

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.

20. On June 30th, 2006 at 5:46 pm Jon said:

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 :-)

21. On July 15th, 2006 at 8:52 pm Max Shirshin said:

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(' ');

22. On August 7th, 2006 at 12:19 pm Arrix said:

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

23. On August 7th, 2006 at 3:40 pm Stuart Colville said:

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

24. On August 15th, 2006 at 2:11 pm Dr Phil said:

Does not work in Opera 9. sux

25. On August 15th, 2006 at 2:43 pm Dr Phil said:

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!

26. On August 15th, 2006 at 3:09 pm Stuart Colville said:

@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.

27. On November 9th, 2006 at 9:13 am Dan said:

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.

28. On November 28th, 2006 at 6:47 pm Eric Shepherd said:

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.

29. On November 29th, 2006 at 2:42 am Stuart Colville said:

@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.

30. On January 18th, 2007 at 6:00 pm Gavy said:

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!

31. On January 25th, 2007 at 2:22 am Gary said:

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.

32. On January 25th, 2007 at 10:20 am Stuart Colville said:

@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.

33. On March 12th, 2007 at 9:15 am javascriptn00b said:

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

34. On March 12th, 2007 at 2:06 pm Stuart Colville said:

@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';
}
35. On March 21st, 2007 at 9:09 pm riptide said:

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

36. On May 11th, 2007 at 4:50 pm k-ny said:

Great tools !

37. On August 31st, 2007 at 2:17 pm brad said:

Hi !

Really great ! Thanks for this !

38. On February 19th, 2008 at 7:07 am Lessan said:

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.

39. On June 9th, 2008 at 11:53 am Nick said:

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.

40. On July 27th, 2009 at 11:23 am Boo said:

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







XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



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

Ubuntu: add-apt-repository: command not found|(2)

When you’re using a minimal Ubuntu install if you find the ‘add-apt-repository’ command is missing (it’s useful for adding PPAs and other repositories), then simply run:

sudo apt-get install python-software-properties

Photos on Flickr

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