Accessing the DOM is not equal accessing the DOM – live vs. static element collections
- 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
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.
// <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
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!
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
. 😲
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...
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
or element
.
// <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.
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.
Join 5.2k readers and learn something new every week with Web Weekly.