dean.edwards.name/weblog/2009/12/getelementsbytagname

getElementsByTagName()

It turns out that document fragments do not implement the getElementsByTagName() method. But they do implement the Selectors API methods: querySelector/querySelectorAll.

Nobody cares about this except for people that write JavaScript selector engines. If you are one of those people then keep reading.

I can code around the absence of a lot of DOM methods but getElementsByTagName() seemed impossible to do without. So I had to fake it!

JavaScript performance

JavaScript is a nice enough scripting language but it is still a scripting language.

For high performance loops you should try to avoid the following:

  1. try/catch blocks
  2. function/method calls (even on native objects)
  3. accessing DOM properties (or anything that involves calling a complex getter/setter)

Please note that this is an ordered list.:)

The code

With all that in mind I wrote this simple implementation of getElementsByTagName() which I hope is fast enough for those edge cases where users execute DOM queries on document fragments:

function getElementsByTagName(node, tagName) {
  var elements = [], i = 0, anyTag = tagName === "*", next = node.firstChild;
  while ((node = next)) {
    if (anyTag ? node.nodeType === 1 : node.nodeName === tagName) elements[i++] = node;
    next = node.firstChild || node.nextSibling;
    while (!next && (node = node.parentNode)) next = node.nextSibling;
  }
  return elements;
}

Update: I’ve amended the original code based on the comments below.

Some things to consider:

Comments (24)

Leave a comment

You can probably optimize a bit more by avoiding double `node.parentNode` access. E.g.:

function getElementsByTagName(node, tagName) {
  var elements = [], i = 0, p;
  while (node) {
    if (tagName == "*" ? node.nodeType == 1 : node.nodeName == tagName) elements[i++] = node;
    node = node.firstChild || node.nextSibling || ((p = node.parentNode) && p.nextSibling);
  }
  return elements;
};
  • Comment by: kangax
  • Posted:

NICE!

a=document.createDocumentFragment();
b=document.createElement("div");
b.innerHTML="<div></div><div></div>"
a.appendChild(b);
alert(getElementsByTagName(a,"DIV"))//3
  • Comment by: caii
  • Posted:

I guess this completely overlooks the idea that the NodeList is supposed to be “live”, but what can you expect for a JS shim… I like the way you did the traversal there!:)

Note that IE does support getElementsByTagName on document fragments. Assuming that it performs well, I check if the built-in function exists and use it if it does, else use my fallback (which wasn’t nearly as slick as yours).

  • Comment by: Jens
  • Posted:

Might still need some work:

a=document.createDocumentFragment();
b=document.createElement("div");
b.innerHTML="

Para

"; a.appendChild(b); alert(getElementsByTagName(a,"DIV").length); //2, but should be 3
  • Comment by: Sean Hogan
  • Posted:

There’s a logic error in the line:

node = node.firstChild || node.nextSibling || (node.parentNode && node.parentNode.nextSibling);

It’ll exit the loop when node’s parentNode doesn’t have a nextSibling, though there are plenty of cases where this doesn’t mean all nodes have been visited. E.g. it’ll miss <d> in <a><b><c></c></b></a><d></d>.

To fix it, you’d need to replace that line with something like:

if (node.firstChild) {
  node = node.firstChild;
} else {
  while (node = node.parentNode) {
    if (node.nextSibling) {
      node = node.nextSibling;
      break;
    }
  }
}
  • Comment by: Scott
  • Posted:

If you’re focused on optimization, why use equivalence testing (==) when you could use equality testing (===)? A quick test in Firefox 3.5 shows that “*” == “*” takes about 40% longer than “*” === “*”. Might not be much compared to the DOM accessing, but hey, it’s a free optimization, and you’re doing three of them!

Equally, if performance is the goal, why have an if statement that checks something unchanging out of the loop? Certainly it doubles the size of the code, but it removes a needless comparison.

  • Comment by: Jim Ley
  • Posted:

@Scott : you may wish to stop in your parent node loop at the context node supplied to the function.

@Jakob/Jim : I have also suggested memoising the the “*”

function getElementsByTagName(node, tagName) 
{
     var elements = [], i = 0, anyTag = (tagName === "*"), root = node, next;
     do
    {
         if (node.nodeType === 1 && (anyTag || node.nodeName === tagName)) elements[i++] = node;

         if (!(next = node.firstChild || node.nextSibling)) 
        {
             while((node = node.parentNode) && node !== root && !(next = node.nextSibling)) {}
        }
    } while(node = next);
  	return elements;
}
  • Comment by: Julian Turner
  • Posted:

@Jakob

Last time I tried testing the performance of ‘==’ vs. ‘===’ I didn’t notice any difference. In theory it should be faster because the interpreter has less to think about, but that would depend on how the interpreter has been coded I guess.

  • Comment by: Pete B
  • Posted:

Forgot the case where the supplied node is empty.

function getElementsByTagName(node, tagName) 
{
 	var elements = [], i = 0, anyTag = (tagName === "*"), root = node, next;
	if (!node.firstChild) { elements[i] = node; return elements;}
  	do
	{
    		if (node.nodeType === 1 && (anyTag || node.nodeName === tagName)) elements[i++] = node;
		while(!(next = node.firstChild) && !(next = node.nextSibling) && (node = node.parentNode) && node !== root && !(next = node.nextSibling)) {}

    	} while(node = next);
  	return elements;
}
  • Comment by: Julian Turner
  • Posted:

Final attempt (forgetting that the context node should not be returned):-

function getElementsByTagName(node, tagName) 
{
 	var elements = [], i = 0, anyTag = (tagName === "*"), root = node, next = node.firstChild;

  	while (node = next)
	{
    		if (node.nodeType === 1 && (anyTag || node.nodeName === tagName)) elements[i++] = node;
		if (next = node.firstChild || node.nextSibling) continue;
		while((node = node.parentNode) && node !== root && !(next = node.nextSibling)) { /* empty */}
    	} 

  	return elements;
}
  • Comment by: Julian Turner
  • Posted:

@Jakob and Pete: I suppose in using “===” we would need to be wary of the unlikely case : “a” !== (new String(“a”))

  • Comment by: Julian Turner
  • Posted:

Thanks for the feedback everybody, particularly Julian. I’ve updated my code based on your suggestions.

  • Comment by: -dean
  • Posted:

I was doing that this way, but your implementation is faster. Thanks!

function getElementsByTagName( node, tagName ){

};
  • Comment by: Ivailo Hristov
  • Posted:

Another thing to avoid for performance is deleting properties (in all the latest engines, that’s something that is really expensive, due to the representation of objects internally).

It could be a nice NodeList with this method :

elements.item = function (i) { return this[i]; };

;)

  • Comment by: Lau S.
  • Posted:

[…] getElementsByTagName() […]

Hi Dean, any bench against this, which is live as well?

function getElementsByTagName(node, nodeName){
    var tmp = document.createElement("tmp");
    return tmp.appendChild(node) && tmp.getElementsByTagName(nodeName);
};

@Andrea (WebReflection)

The node list doesn’t need to be live. This is an internal method just for use by selector engines.

Your solution is a good idea but unfortunately it does not work because of a behaviour unique to document fragments. Once you append a document fragment to an element, the fragment disappears and the fragment’s children become direct children of the element that the fragment is appended to. I hope I explained that properly.:)

  • Comment by: -dean
  • Posted:

still me, I kinda realize how silly was my snippet the instant after I pressed submit:D

so … what about this? Still talking about performances, I do like your solution.

function getElementsByTagName(node, nodeName){
    for(var
        tmp = document.createElement("tmp"),
        childNodes = tmp.appendChild(node) && tmp.getElementsByTagName(nodeName),
        i = 0, length = childNodes.length,
        result = [];
        i < length; ++i
    )
        result[i] = childNodes[i]
    ;
    childNodes = tmp.childNodes;
    length = childNodes.length;
    while(length)
        node.appendChild(childNodes[--length])
    ;
    return tmp = result;
};

I need some rest …

function getElementsByTagName(node, nodeName){
    for(var
        tmp = document.createElement("tmp"),
        childNodes = tmp.appendChild(node) && tmp.getElementsByTagName(nodeName),
        i = 0, length = childNodes.length,
        result = [];
        i < length; ++i
    )
        result[i] = childNodes[i]
    ;
    childNodes = tmp.childNodes;
    length = childNodes.length;
    while(length--)
        node.appendChild(childNodes[0])
    ;
    return tmp = result;
};

could be probably smaller via while ...

    while(childNodes = tmp.firstChild)
        node.appendChild(childNodes)
    ;

not sure which one will perform better though ... I'll try some bench later;-)

[…] Quelle hier […]

Note that IE does support getElementsByTagName on document fragments., and since this post was written in 2010, and its 2015 now, There is a goods chances that its already implemented in all the other frameworks.

  • Comment by: Ran Geller
  • Posted:

Comments are closed.