270

I was wondering, JavaScript offers a variety of methods to get the first child element from any element, but which is the best? By best, I mean: most cross-browser compatible, fastest, most comprehensive and predictable when it comes to behaviour. A list of methods/properties I use as aliases:

var elem = document.getElementById('container');
var child = elem.children[0];
var child = elem.firstElementChild; // == children[0]

This works for both cases:

var child = elem.childNodes[0]; // or childNodes[1], see below

That’s in case of forms, or <div> iteration. If I might encounter text elements:

var child = elem.childNodes; // treat as NodeList
var child = elem.firstChild;

As far as I can work out, firstChild uses the NodeList from childNodes, and firstElementChild uses children. I’m basing this assumption on the MDN reference:

childNode is a reference to the first child element of the element node, or null if there isn’t one.

I’m guessing that, in terms of speed, the difference, if any, will be next to nothing, since firstElementChild is effectively a reference to children[0], and the children object is already in memory anyway.

What does throw me, is the childNodes object. I’ve used it to take a look at a form, in a table element. While children lists all form elements, childNodes also seems to include whitespace from the HTML code:

console.log(elem.childNodes[0]);
console.log(elem.firstChild);

Both log <TextNode textContent="\n ">

console.log(elem.childNodes[1]);
console.log(elem.children[0]);
console.log(elem.firstElementChild);

All log <input type="text">. How come? I’d understand that one object would allow me to work with the “raw” HTML code, while the other sticks to the DOM, but the childNodes element seems to work on both levels.

To get back to my initial question, my guess would be: if I want the most comprehensive object, childNodes is the way to go, but because of its comprehensiveness, it might not be the most predictable in terms of it returning the element I want/expect at any given moment. Cross-browser support might also prove to be a challenge in that case, though I could be wrong.

Could anyone clarify the distinction between the objects at hand? If there is a speed difference, however negligible, I’d like to know, too. If I’m seeing this all wrong, feel free to educate me.


PS: Please, please, I like JavaScript, so yes, I want to deal with this sort of thing. Answers like “jQuery deals with this for you” are not what I’m looking for, hence no tag.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149

5 Answers5

253

Sounds like you're overthinking it. You've observed the difference between childNodes and children, which is that childNodes contains all nodes, including text nodes consisting entirely of whitespace, while children is a collection of just the child nodes that are elements. That's really all there is to it.

There is nothing unpredictable about either collection, although there are a couple of issues to be aware of:

  • IE <= 8 do not include white space-only text nodes in childNodes while other browsers do
  • IE <= 8 includes comment nodes within children while other browsers only have elements

children, firstElementChild and friends are just conveniences, presenting a filtered view of the DOM restricted to just elements.

Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • Figured as much, though one thing still bugs me: the whitespace textnode that childNodes picks up on is actually just a new line and a couple of tabs in the code. Is this due to the XHTML doctype? can this be resolved by enclosing the whitespace to struct code within the tags? – Elias Van Ootegem Apr 30 '12 at 10:07
  • @EliasVanOotegem: That's unrelated to doctype. White space text nodes are always included in the DOM (except in IE < 9) for both HTML and XHTML, wherever they appear in the source. It's generally a good thing, although it can catch you out if you're not prepared for it. – Tim Down Apr 30 '12 at 10:32
  • I meant if it would help to write my markup like this `content`. As you might do with XML, to avoid excess whitespace being regarded as part of the data (or DOM, in this case). Why would whitespace inside a tag be included in the DOM? – Elias Van Ootegem Apr 30 '12 at 10:42
  • @EliasVanOotegem: Oh, I see. White space after the tag name inside a tag is ignored, so you could do that kind of thing. I've seen that technique used in precise layouts to prevent browsers adding tiny gaps for white space between some kinds of elements. – Tim Down Apr 30 '12 at 10:48
  • Ok, thanks. PS: nice work on log4javascript BTW. Had a look at your profile. That's the level of JS knowledge I hope to achieve, hence I'm at the stage of overcomplicating and over analysing every single thing I encounter :-D – Elias Van Ootegem Apr 30 '12 at 10:56
  • @TimDown according to quirksmode, it's more complicated than that, as soon as you need cross-browser and especially IE<9: http://quirksmode.org/dom/core/ – Christophe Sep 11 '13 at 19:15
  • @Christophe: How so? I've alluded to the issue in IE <= 8 to do with white space-only text nodes in my answer. – Tim Down Sep 11 '13 at 22:18
  • @TimDown another issue reported by quirksmode: in children(), IE8 and lower incorrectly count comment nodes (I just happen to have such nodes in my current project) – Christophe Sep 11 '13 at 22:35
  • 2
    @Christophe: Ah, fair point, thanks. I'll add a note to my answer. Given that `children` originated in IE 4 over a decade before becoming a standard or being adopted by other browsers, you could argue that IE <= 8 is correct and other browsers are wrong. – Tim Down Sep 12 '13 at 08:26
  • Useful to note that using a `for in` loop on `.children` will throw unexpected results as there are named keys on the children object. Namely `length` and the `__proto__`. Probably obvious but it threw me for a while... – shennan Feb 15 '14 at 11:17
  • @shennan: That's why, in my question, I wrote _"treat as Nodelist"_ in a comment. a `NodeList` is an array-like object, and some browsers (like FF) implement a custom object for this, as part of their super-set of JS and the DOM API. But the point is: if it's an array-like instance, don't use `for...in`, or filter the loop using `for (p in inst) { if (inst.hasOwnProperty(p)) { /* do stuff */}}` – Elias Van Ootegem Apr 02 '14 at 07:19
  • Concerning the empty text nodes, javascript has the normalize() method to aid in removal of empty text nodes and joining of adjacent Text nodes: [normalize()](http://www.w3schools.com/jsref/met_document_normalize.asp) – "The normalize() method removes empty Text nodes, and joins adjacent Text nodes." – CR Rollyson Nov 04 '16 at 11:04
24

firstElementChild might not be available in IE<9 (only firstChild)

on IE<9 firstChild is the firstElementChild because MS DOM (IE<9) is not storing empty text nodes. But if you do so on other browsers they will return empty text nodes...

my solution

child=(elem.firstElementChild||elem.firstChild)

this will give the firstchild even on IE<9

neu-rah
  • 1,662
  • 19
  • 32
  • If `elem.firstChild` is not an element node, the results will be different in old IEs, because `elem.firstElementChild` is always element node. – duri Apr 30 '12 at 09:36
  • I'm sorry, but I know how to get the first child in all major browsers. What I want to know is how the different objects are structured, in order to get a better understanding of the similarities and differences between them. I'll edit my question later to make that clearer, sorry for the mixup – Elias Van Ootegem Apr 30 '12 at 09:37
  • 1
    `firstElementChild` is not necessarily safe in non-IE browsers either: Firefox only started supporting it in version 3.5. – Tim Down Apr 30 '12 at 09:49
  • this is working for chrome, IE9 and IE8 because if they have the firstElementChild then the other check is not executed, otherwise(old IE) we have the firstChild, and it is an element node for browser that do not have firstElementChild (IE<9)... still i wonder about FF – neu-rah Apr 30 '12 at 10:03
  • 1
    Tim is correct, when I was googling stuff on these objects [this](http://help.dottoro.com/ljitlpua.php) showed up, good site to check once in a while – Elias Van Ootegem Apr 30 '12 at 10:19
  • +1 exactly what I needed. And contrary to the accepted answer it takes into account IE<9. – Christophe Sep 11 '13 at 19:30
  • @Christophe: A little harsh: this answer does not take IE < 9 into account any more than mine. – Tim Down Sep 12 '13 at 08:28
  • @TimDown I didn't mean to be rude, sorry if it sounded like that. I am focused on my current project, and children() just doesn't make the cut because of the way it deals with comments in IE<9 (again, based on what I read on Quirksmode). – Christophe Sep 12 '13 at 15:26
  • Works a dream, fixed a double click bug I had in a carousel http://jsfiddle.net/xxwatcherxx/4tbyxk53/50/ ...cheers...I owe you a pint... –  Nov 11 '14 at 00:11
23

The cross browser way to do is to use childNodes to get NodeList, then make an array of all nodes with nodeType ELEMENT_NODE.

/**
 * Return direct children elements.
 *
 * @param {HTMLElement}
 * @return {Array}
 */
function elementChildren (element) {
    var childNodes = element.childNodes,
        children = [],
        i = childNodes.length;

    while (i--) {
        if (childNodes[i].nodeType == 1) {
            children.unshift(childNodes[i]);
        }
    }

    return children;
}

http://jsfiddle.net/s4kxnahu/

This is especially easy if you are using a utility library such as lodash:

/**
 * Return direct children elements.
 *
 * @param {HTMLElement}
 * @return {Array}
 */
function elementChildren (element) {
    return _.where(element.childNodes, {nodeType: 1});
}

Future:

You can use querySelectorAll in combination with :scope pseudo-class (matches the element that is the reference point of the selector):

parentElement.querySelectorAll(':scope > *');

At the time of writing this :scope is supported in Chrome, Firefox and Safari.

Gajus
  • 69,002
  • 70
  • 275
  • 438
  • [:scope was removed from the spec](https://github.com/whatwg/html/issues/552). Should we remove the **Future** section? – Thomas Marti Sep 08 '17 at 16:46
  • Nope it was ` – Rafa Viotti Oct 31 '20 at 16:54
5

Just to add to the other answers, there are still noteworthy differences here, specifically when dealing with <svg> elements.

I have used both .childNodes and .children and have preferred working with the HTMLCollection delivered by the .children getter.

Today however, I ran into issues with IE/Edge failing when using .children on an <svg>. While .children is supported in IE on basic HTML elements, it isn't supported on document/document fragments, or SVG elements.

For me, I was able to simply grab the needed elements via .childNodes[n] because I don't have extraneous text nodes to worry about. You may be able to do the same, but as mentioned elsewhere above, don't forget that you may run into unexpected elements.

Hope this is helpful to someone scratching their head trying to figure out why .children works elsewhere in their js on modern IE and fails on document or SVG elements.

slothluvchunk
  • 382
  • 2
  • 9
4

Don't let white space fool you. Just test this in a console browser.

Use native javascript. Here is and example with two 'ul' sets with the same class. You don't need to have your 'ul' list all in one line to avoid white space just use your array count to jump over white space.

How to get around white space with querySelector() then childNodes[] js fiddle link: https://jsfiddle.net/aparadise/56njekdo/

var y = document.querySelector('.list');
var myNode = y.childNodes[11].style.backgroundColor='red';

<ul class="list">
    <li>8</li>
    <li>9</li>
    <li>100</li>
</ul>

<ul class="list">
    <li>ABC</li>
    <li>DEF</li>
    <li>XYZ</li>
</ul>
peterh
  • 11,875
  • 18
  • 85
  • 108
  • 1
    Not my down-vote, but I think somebody down-voted because: a) This question is 4 years old, `document.querySelector` wasn't well supported back then. b) The question is basically asking what the differences are between `children` and `childNodes` _internally_, which is more reliable, when to choose one over the other. and c) Because, as demonstraded in the question: `childNodes` _does_ include whitespace nodes, `children` does not... – Elias Van Ootegem Mar 31 '16 at 09:51