Published at
Updated at
Reading time
4min

When a browser parses an HTML document, it creates the Document Object Model (DOM). HTML elements are represented as DOM tree elements that you can access programmatically in JavaScript.

document.querySelectorAll is one of these DOM access methods, but it's not the only one. Let's look at the others methods and find some suprises.

Accessing a NodeList using querySelectorAll

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

const listItems = document.querySelectorAll('li');
console.log(listItems);        // NodeList(3) [li, li, li]
console.log(listItems.length); // 3

for (let i = 0; i < listItems.length; i++) {
  console.log(listItems[i].innerText);
}

// foo
// bar
// baz

If you log what is returned by document.querySelectorAll you'll see that you're dealing with a NodeList.

NodeLists look like JavaScript Arrays, but they are not. If you read the NodeList MDN article, it describes this fact clearly.

Although NodeList is not an Array, it is possible to iterate on it using forEach(). Several older browsers have not implemented this method yet. You can also convert it to an Array using Array.from.

Surprisingly, NodeLists provide a forEach method. This method was missing when I started working in web development, and it was one of the pitfalls I ran into a lot over the years.

Additionally, a NodeList provides other "Array-like" methods such as item, entries, keys, and values. Read more about these details in the MDN article.

querySelectorAll is only one way to access the DOM, though. Let's move on to learn more!

The magic of live collections

If you read the NodeList documentation, you might have noticed "a little fun detail":

In some cases, the NodeList is a live collection [...]

Oh boy...

Wait, what? A live collection? In some cases?

It turns out that NodeLists behave differently depending on how you access them. Let's have a look at the same document and access DOM elements differently.

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

// retrieve element using querySelectorAll
const listItems_querySelectorAll = document.querySelectorAll('li');
console.log(listItems_querySelectorAll); // NodeList(3) [li, li, li]

// retrieve element using childNodes
const list  = document.querySelector('ul');
const listItems_childNodes = list.childNodes;
console.log(listItems_childNodes); // NodeList(7) [text, li, text, li, text, li, text]

A NodeList accessed via childNodes includes more elements than a NodeList returned by document.querySelectorAll. 😲

childNodes includes text nodes such as spaces and line breaks.

console.log(listItems_childNodes[0].textContent) // "↵  "

But that's only the first difference. It turns out that NodeLists' can be "live" or "static", too.

Let's add another item to the queried list and see what happens.

list.appendChild(document.createElement('li'));

// static NodeList accessed via querySelectorAll
console.log(listItems_querySelectorAll); // NodeList(3) [li, li, li]

// live NodeList accessed via childNodes
console.log(listItems_childNodes);       // NodeList(8) [text, li, text, li, text, li, text, li]

😲 As you see listItems_childNodes (the NodeList accessed via childNodes) reflects the elements of the DOM even when elements were added or removed. It's "live".

The NodeList collection returned by querySelectorAll stays the same. It's a representation of the elements when the DOM was queried.

That's already quite confusing, but hold on. We're not done yet...

Not every method to query the DOM returns a NodeList

You might know that there are more methods to query the DOM. getElementsByClassName and getElementsByTagName let you access DOM elements, too.

And it turns out that these methods return something entirely different.

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

const listItems_getElementsByTagName = document.getElementsByTagName('li');
console.log(listItems_getElementsByTagName); // HTMLCollection(3) [li, li, li]

Oh well... an HTMLCollection?

An HTMLCollection only includes matching elements (no text nodes), it provides only two methods (item and namedItem) and it is live which means that it will reflect added and removed DOM elements.

// add a new item to the list
listItems_getElementsByTagName[0].parentNode.appendChild(document.createElement('li'));

// live HTMLCollection accessed via getElementsByTagName
console.log(listItems_getElementsByTagName); // HTMLCollection(4) [li, li, li, li]

And to make it even more complicated, HTMLCollections are also returned when you access the DOM using properties such as document.forms or element.children.

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

const list = document.querySelector('ul');
const listItems = list.children;
console.log(listItems); // HTMLCollection [li, li, li]

Look at the specification of HTMLCollection and find the following sentence:

HTMLCollection is a historical artifact we cannot rid the web of. While developers are of course welcome to keep using it, new API standard designers ought not to use it [...]

NodeList and HTMLCollection where competing standards and now we're stuck with both of them because we can't break the web by removing functionality.

Evolving the web is complicated

So in summary; today there are DOM element properties such as childNodes (returning a live NodeList) and children (returning a live HTMLCollection), methods like querySelectorAll (returning a static NodeList) and getElementsByTagName (returning a live HTMLCollection). Accessing the DOM is not equal to accessing the DOM!

I haven't heard of live and static collections before, but this DOM access discovery will save me a lot of time in the future because finding a bug caused by a live collection is very hard to spot.

If you want to play around with the described behavior, check this CodePen.

If you enjoyed this article...

Join 5.2k readers and learn something new every week with Web Weekly.

Web Weekly — Your friendly Web Dev newsletter
Stefan standing in the park in front of a green background

About Stefan Judis

Frontend nerd with over ten years of experience, freelance dev, "Today I Learned" blogger, conference speaker, and Open Source maintainer.

Related Topics

Related Articles