JavaScript DOM Collections

When you query the DOM for multiple elements, you get back a collection: either an HTMLCollection or a NodeList. Understanding the differences between these two collection types is important for correctly working with DOM elements.

HTMLCollection vs NodeList

HTMLCollection and NodeList are both array-like objects that hold DOM elements, but they have key differences in how they behave and what they contain.

FeatureHTMLCollectionNodeList
ContainsElements onlyAny node type
Live/StaticAlways liveUsually static (querySelectorAll)
Access by nameYes (.namedItem())No
forEach()No (must convert)Yes
Returned bygetElementsBy*querySelectorAll, childNodes
HTMLCollection vs NodeList
// HTMLCollection (live)
// const byTag = document.getElementsByTagName('p');
// const byClass = document.getElementsByClassName('item');

// NodeList (static)
// const byQuery = document.querySelectorAll('p');
// const childList = element.childNodes;

// Key difference: live vs static
console.log('HTMLCollection is LIVE:');
console.log('  - Automatically updates when DOM changes');
console.log('  - Length changes if elements are added/removed');

console.log('\nNodeList from querySelectorAll is STATIC:');
console.log('  - Snapshot at time of query');
console.log('  - Does NOT update when DOM changes');

console.log('\nNodeList from childNodes is LIVE');
console.log('  - Updates when children change');

Live vs Static Collections

A live collection automatically reflects changes to the DOM. A static collection is a snapshot taken at the time of the query and does not change when the DOM is modified.

Live vs Static Behavior
// Live collection example
// const items = document.getElementsByClassName('item');
// console.log(items.length);  // e.g., 3
// // Add a new element with class 'item'
// document.body.innerHTML += '<div class="item">New</div>';
// console.log(items.length);  // 4 - automatically updated!

// Static collection example
// const items = document.querySelectorAll('.item');
// console.log(items.length);  // 3
// document.body.innerHTML += '<div class="item">New</div>';
// console.log(items.length);  // Still 3 - snapshot!

// Simulating the difference
let liveCollection = [1, 2, 3];
const staticCollection = [1, 2, 3];  // Snapshot

liveCollection.push(4);
console.log('Live after adding: ' + liveCollection);
console.log('Static (unchanged): ' + staticCollection);

console.log('\nBe careful with live collections in loops!');
console.log('Adding/removing elements can cause infinite loops');

Converting to Array

Both HTMLCollection and NodeList are array-like objects but are not true arrays. To use array methods like map, filter, and reduce, you need to convert them to arrays.

Converting Collections to Arrays
// Method 1: Array.from()
// const arr1 = Array.from(document.querySelectorAll('p'));

// Method 2: Spread operator
// const arr2 = [...document.querySelectorAll('p')];

// Method 3: Array.prototype.slice.call()
// const arr3 = Array.prototype.slice.call(collection);

// Simulating conversion
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };

// Convert to real array
const arr = Array.from(arrayLike);
console.log('Is array: ' + Array.isArray(arr));
console.log('Array: ' + arr);

// Now you can use array methods
const upper = arr.map(function(item) {
  return item.toUpperCase();
});
console.log('Mapped: ' + upper);

const filtered = arr.filter(function(item) {
  return item !== 'b';
});
console.log('Filtered: ' + filtered);

Iterating Collections

NodeList supports forEach() directly. HTMLCollection does not. Both support for...of loops and index-based access.

Iterating Over Collections
// NodeList - supports forEach
// document.querySelectorAll('p').forEach(function(p, i) {
//   console.log(i + ': ' + p.textContent);
// });

// HTMLCollection - no forEach, use for loop or convert
// const items = document.getElementsByTagName('li');
// for (let i = 0; i < items.length; i++) {
//   console.log(items[i].textContent);
// }

// for...of works with both
// for (const item of items) {
//   console.log(item.textContent);
// }

const items = ['Apple', 'Banana', 'Cherry'];

// forEach (NodeList)
console.log('forEach:');
items.forEach(function(item, i) {
  console.log('  ' + i + ': ' + item);
});

// for...of (both)
console.log('for...of:');
for (const item of items) {
  console.log('  ' + item);
}

Which Methods Return What

Knowing which methods return HTMLCollection and which return NodeList helps you predict the collection's behavior.

Return Types by Method
// Returns HTMLCollection (LIVE):
console.log('HTMLCollection (live):');
console.log('  document.getElementsByTagName("p")');
console.log('  document.getElementsByClassName("cls")');
console.log('  element.children');
console.log('  document.forms');
console.log('  document.images');

// Returns NodeList (STATIC from querySelectorAll):
console.log('\nNodeList (static):');
console.log('  document.querySelectorAll("p")');

// Returns NodeList (LIVE):
console.log('\nNodeList (live):');
console.log('  element.childNodes');

// Returns single Element:
console.log('\nSingle Element:');
console.log('  document.getElementById("id")');
console.log('  document.querySelector(".class")');
📝 Note: querySelectorAll() is generally preferred over getElementsBy* methods because it returns a static NodeList (safer to iterate) and supports complex CSS selectors. However, getElementsBy* methods can be slightly faster for simple queries and provide live collections when that behavior is needed.
Exercise:
What does querySelectorAll() return?
Try it YourselfCtrl+Enter to run
Click Run to see the output here.