Skip to content

DOM Introduction and Tree Structure: Understanding the Skeleton of Web Pages

Understanding DOM

Open any webpage, and every headline, paragraph, image, and button you see is supported by a structured data behind it. When the browser loads an HTML document, it converts these tags into a collection of objects that can be manipulated by JavaScript - this is the DOM (Document Object Model).

If we compare a webpage to a big tree, HTML tags are the branches and leaves on the tree, while the DOM is the complete map of this tree. With this map, JavaScript can precisely find any "leaf", modify its color and shape, or even pick it off or plant new branches.

DOM is not part of the JavaScript language, nor is it part of HTML. It is a set of standard interfaces developed by W3C that allows programming languages to access and manipulate the structure, style, and content of HTML or XML documents. Browsers implement the DOM API according to this standard, and JavaScript interacts with pages through these APIs.

The Essence of DOM

DOM represents the entire document as a tree structure composed of nodes. Each HTML element, text content, and even comments become a node on this tree.

Let's look at a simple HTML document:

html
<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
  </head>
  <body>
    <h1>Welcome</h1>
    <p>Hello, world!</p>
  </body>
</html>

After the browser parses this code, it builds the following tree structure:

document
└── html
    ├── head
    │   └── title
    │       └── "My Page" (text node)
    └── body
        ├── h1
        │   └── "Welcome" (text node)
        └── p
            └── "Hello, world!" (text node)

In this tree, document is the root node, representing the entire document. The html element is a child node of document and also the parent node of head and body. head and body are sibling nodes to each other. The text content within each tag is also a node, called a text node.

DOM Node Types

DOM defines multiple node types, each with a corresponding numeric constant. In actual development, you'll most commonly deal with the following types:

Element Node

Element nodes correspond to HTML tags and are the most common node type. Each HTML tag creates an element node.

javascript
const heading = document.querySelector("h1");
console.log(heading.nodeType); // 1
console.log(heading.nodeName); // "H1"
console.log(heading.nodeType === Node.ELEMENT_NODE); // true

Element nodes have a nodeType value of 1, and nodeName returns the uppercase tag name.

Text Node

Text nodes contain the text content within elements. Even just a line break or space is treated as a text node.

javascript
const paragraph = document.querySelector("p");
const textNode = paragraph.firstChild;

console.log(textNode.nodeType); // 3
console.log(textNode.nodeName); // "#text"
console.log(textNode.nodeValue); // "Hello, world!"

Text nodes have a nodeType value of 3. To get or modify text content, you need to access the nodeValue or textContent property.

Comment Node

Comments in HTML also become part of the DOM tree:

html
<!-- This is a comment -->
<div id="app">Content</div>
javascript
const app = document.getElementById("app");
const commentNode = app.previousSibling.previousSibling; // May need to skip whitespace text nodes

console.log(commentNode.nodeType); // 8
console.log(commentNode.nodeValue); // " This is a comment "

Comment nodes have a nodeType value of 8.

Document Node

The document object represents the entire document and is the root node of the DOM tree.

javascript
console.log(document.nodeType); // 9
console.log(document.nodeName); // "#document"

Document nodes have a nodeType value of 9 and are the starting point for accessing any element on the page.

Node Type Quick Reference

TypeConstant NamenodeType ValuenodeNamenodeValue
Element NodeELEMENT_NODE1Tag name (UPPERCASE)null
Text NodeTEXT_NODE3#textText content
Comment NodeCOMMENT_NODE8#commentComment content
Document NodeDOCUMENT_NODE9#documentnull
Document Type NodeDOCUMENT_TYPE_NODE10Document type namenull
Document Fragment NodeDOCUMENT_FRAGMENT_NODE11#document-fragmentnull

Relationships Between Nodes

Nodes in the DOM tree are interconnected through a series of properties, just like family relationships in a genealogy.

Parent-Child Relationships

Each node (except document) has one parent node and may have zero or more child nodes.

javascript
const list = document.querySelector("ul");

// Get parent node
console.log(list.parentNode); // Parent element
console.log(list.parentElement); // Parent element (always an element node)

// Get child nodes
console.log(list.childNodes); // All child nodes (including text nodes)
console.log(list.children); // Only element child nodes

// First and last child nodes
console.log(list.firstChild); // First child node (may be a text node)
console.log(list.firstElementChild); // First element child node
console.log(list.lastChild); // Last child node
console.log(list.lastElementChild); // Last element child node

childNodes returns a NodeList containing all types of child nodes, including whitespace text nodes between elements. children returns an HTMLCollection containing only element nodes, which is more practical in most cases.

Sibling Relationships

Nodes under the same parent are siblings to each other:

javascript
const currentItem = document.querySelector("#item-2");

// Get adjacent siblings
console.log(currentItem.previousSibling); // Previous sibling node (may be a text node)
console.log(currentItem.previousElementSibling); // Previous element sibling
console.log(currentItem.nextSibling); // Next sibling node
console.log(currentItem.nextElementSibling); // Next element sibling

Properties with Element skip text and comment nodes and return element nodes directly, making them more convenient for daily use.

Relationship Properties Comparison

PropertyReturns All Node TypesOnly Element Nodes
Parent nodeparentNodeparentElement
Child nodes listchildNodeschildren
First child nodefirstChildfirstElementChild
Last child nodelastChildlastElementChild
Previous siblingpreviousSiblingpreviousElementSibling
Next siblingnextSiblingnextElementSibling

Traversing the DOM Tree

In actual development, you often need to traverse the DOM tree to find or process nodes. Here are several common traversal methods.

Traversing Child Nodes

javascript
const container = document.querySelector(".container");

// Method 1: Using for loop
for (let i = 0; i < container.children.length; i++) {
  console.log(container.children[i]);
}

// Method 2: Using for...of loop
for (const child of container.children) {
  console.log(child);
}

// Method 3: Convert to array and use forEach
Array.from(container.children).forEach((child) => {
  console.log(child);
});

// Method 4: Using spread operator
[...container.children].forEach((child) => {
  console.log(child);
});

Note that children and childNodes both return "live" collections. When the DOM changes, the collection content updates automatically. Modifying the DOM during traversal may lead to unexpected results. A safe approach is to first convert the collection to a static array.

Recursively Traverse All Descendant Nodes

When you need to traverse all descendants of an element, recursion is an effective method:

javascript
function walkDOM(node, callback) {
  callback(node);

  for (const child of node.children) {
    walkDOM(child, callback);
  }
}

// Usage example: Print all element tag names
const root = document.body;
walkDOM(root, (node) => {
  console.log(node.tagName);
});

This function starts from a given node, first executes a callback on the current node, then recursively processes each child element.

Using TreeWalker

DOM provides the TreeWalker API, designed specifically for efficiently traversing the DOM tree:

javascript
const walker = document.createTreeWalker(
  document.body, // Starting node
  NodeFilter.SHOW_ELEMENT, // Only show element nodes
  null, // Optional filter function
  false // Whether to expand entity references (deprecated)
);

// Traverse all elements
let currentNode;
while ((currentNode = walker.nextNode())) {
  console.log(currentNode.tagName);
}

NodeFilter can specify the types of nodes to traverse:

  • NodeFilter.SHOW_ALL: All nodes
  • NodeFilter.SHOW_ELEMENT: Element nodes
  • NodeFilter.SHOW_TEXT: Text nodes
  • NodeFilter.SHOW_COMMENT: Comment nodes

You can also pass a custom filter function for more fine-grained control over traversal behavior:

javascript
const walker = document.createTreeWalker(
  document.body,
  NodeFilter.SHOW_ELEMENT,
  {
    acceptNode(node) {
      // Only accept elements with data-interactive attribute
      if (node.hasAttribute("data-interactive")) {
        return NodeFilter.FILTER_ACCEPT;
      }
      return NodeFilter.FILTER_SKIP;
    },
  }
);

The document Object

document is the entry point for DOM manipulation and provides many useful properties and methods.

Common Properties

javascript
// Document information
console.log(document.title); // Page title
console.log(document.URL); // Current URL
console.log(document.domain); // Domain name
console.log(document.referrer); // Referring page

// Document status
console.log(document.readyState); // "loading" | "interactive" | "complete"

// Core element access
console.log(document.documentElement); // <html> element
console.log(document.head); // <head> element
console.log(document.body); // <body> element

// Special collections
console.log(document.forms); // All forms
console.log(document.images); // All images
console.log(document.links); // All <a> and <area> with href
console.log(document.scripts); // All scripts

Document Ready State

Before manipulating the DOM, you need to ensure the document has finished loading. document.readyState has three possible values:

  1. loading: Document is loading
  2. interactive: Document has been parsed, but resources (like images) are still loading
  3. complete: Document and all resources have finished loading
javascript
// Method 1: Check readyState
if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

function init() {
  console.log("DOM is ready");
}

// Method 2: Use DOMContentLoaded event
document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM content loaded");
});

// Method 3: Wait for all resources to load (including images, stylesheets, etc.)
window.addEventListener("load", () => {
  console.log("Page fully loaded");
});

DOMContentLoaded fires immediately after HTML parsing completes, without waiting for stylesheets, images, and other resources. The load event waits for all resources to finish loading. In most cases, using DOMContentLoaded is sufficient and results in faster page response.

Performance Considerations for DOM Operations

DOM operations are relatively expensive operations in frontend development. Each time you read or modify the DOM, the browser may need to recalculate styles, layout, or even redraw the page.

Avoid Frequent DOM Access

javascript
// ❌ Bad practice: Access DOM in every loop iteration
for (let i = 0; i < 1000; i++) {
  document.getElementById("counter").textContent = i;
}

// ✅ Better practice: Cache DOM references
const counter = document.getElementById("counter");
for (let i = 0; i < 1000; i++) {
  counter.textContent = i;
}

Use Document Fragments for Batch Operations

javascript
// ❌ Bad practice: Add elements one by one
const list = document.getElementById("list");
for (let i = 0; i < 100; i++) {
  const item = document.createElement("li");
  item.textContent = `Item ${i}`;
  list.appendChild(item); // Triggers DOM update each time
}

// ✅ Better practice: Use DocumentFragment
const list = document.getElementById("list");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const item = document.createElement("li");
  item.textContent = `Item ${i}`;
  fragment.appendChild(item); // Doesn't trigger DOM update
}
list.appendChild(fragment); // Triggers only one DOM update

DocumentFragment is a lightweight document container that exists in memory and is not part of the actual DOM tree. Operations performed on a fragment don't affect page rendering; only when the entire fragment is inserted into the DOM does it trigger a single update.

NodeList vs HTMLCollection

When working with the DOM, you'll often encounter these two collection types, which have slightly different behaviors.

HTMLCollection

HTMLCollection is a live collection that automatically updates as the DOM changes:

javascript
const divs = document.getElementsByTagName("div");
console.log(divs.length); // Suppose it's 3

// Add a new div
document.body.appendChild(document.createElement("div"));
console.log(divs.length); // Automatically becomes 4

HTMLCollection can access elements by index or the item() method, and can also find elements by id or name attributes using the namedItem() method:

javascript
const forms = document.forms;
const loginForm = forms.namedItem("login");
// Or directly use
const loginForm = forms["login"];

NodeList

NodeList may be live or static, depending on how it was obtained:

javascript
// Live NodeList
const childNodes = document.body.childNodes;

// Static NodeList
const queriedNodes = document.querySelectorAll("div");

querySelectorAll returns a static snapshot that doesn't update as the DOM changes.

Converting Collections to Arrays

Whether HTMLCollection or NodeList, neither is a true array and cannot directly use array methods (like map, filter). You need to convert first:

javascript
const divs = document.querySelectorAll("div");

// Method 1: Array.from()
const divsArray = Array.from(divs);

// Method 2: Spread operator
const divsArray = [...divs];

// Method 3: Array.prototype.slice.call() (compatible with old browsers)
const divsArray = Array.prototype.slice.call(divs);

// Now you can use array methods
divsArray
  .filter((div) => div.classList.contains("active"))
  .forEach((div) => console.log(div));

Practical Application Examples

Building a DOM Tree Visualization Tool

The following code can print the DOM tree structure in a readable format:

javascript
function visualizeDOM(element, indent = 0) {
  const padding = "  ".repeat(indent);
  let output = "";

  // Print element nodes
  if (element.nodeType === Node.ELEMENT_NODE) {
    const tagName = element.tagName.toLowerCase();
    const id = element.id ? `#${element.id}` : "";
    const classes = element.className
      ? `.${element.className.split(" ").join(".")}`
      : "";

    output += `${padding}<${tagName}${id}${classes}>\n`;

    // Recursively process child nodes
    for (const child of element.childNodes) {
      output += visualizeDOM(child, indent + 1);
    }

    output += `${padding}</${tagName}>\n`;
  }
  // Print text nodes (non-whitespace)
  else if (element.nodeType === Node.TEXT_NODE) {
    const text = element.textContent.trim();
    if (text) {
      output += `${padding}"${text}"\n`;
    }
  }
  // Print comment nodes
  else if (element.nodeType === Node.COMMENT_NODE) {
    output += `${padding}<!-- ${element.nodeValue.trim()} -->\n`;
  }

  return output;
}

// Usage example
console.log(visualizeDOM(document.documentElement));

Finding the Path to a Specific Node

Sometimes you need to get the complete path from the root node to an element:

javascript
function getNodePath(element) {
  const path = [];
  let current = element;

  while (current && current !== document) {
    let selector = current.tagName.toLowerCase();

    if (current.id) {
      selector += `#${current.id}`;
    } else if (current.className) {
      selector += `.${current.className.split(" ").join(".")}`;
    }

    // Add index if there are siblings of the same type
    if (current.parentElement) {
      const siblings = current.parentElement.children;
      const sameTypeSiblings = [...siblings].filter(
        (s) => s.tagName === current.tagName
      );
      if (sameTypeSiblings.length > 1) {
        const index = sameTypeSiblings.indexOf(current) + 1;
        selector += `:nth-of-type(${index})`;
      }
    }

    path.unshift(selector);
    current = current.parentElement;
  }

  return path.join(" > ");
}

// Usage example
const element = document.querySelector(".target");
console.log(getNodePath(element));
// Output similar to: "html > body > div#app > main.content > article:nth-of-type(2) > p.target"

Summary

DOM is the foundation for JavaScript to interact with web page content. By understanding the tree structure and node relationships of DOM, you can:

  1. Accurately locate elements: Navigate the DOM tree using parent-child and sibling relationships
  2. Efficiently traverse nodes: Choose appropriate traversal methods based on needs
  3. Understand node types: Distinguish between different types like element nodes, text nodes, etc.
  4. Optimize performance: Reduce unnecessary DOM operations, use techniques like document fragments

Subsequent chapters will detail how to select, create, and modify DOM elements, as well as more practical DOM manipulation techniques.