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:
<!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.
const heading = document.querySelector("h1");
console.log(heading.nodeType); // 1
console.log(heading.nodeName); // "H1"
console.log(heading.nodeType === Node.ELEMENT_NODE); // trueElement 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.
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:
<!-- This is a comment -->
<div id="app">Content</div>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.
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
| Type | Constant Name | nodeType Value | nodeName | nodeValue |
|---|---|---|---|---|
| Element Node | ELEMENT_NODE | 1 | Tag name (UPPERCASE) | null |
| Text Node | TEXT_NODE | 3 | #text | Text content |
| Comment Node | COMMENT_NODE | 8 | #comment | Comment content |
| Document Node | DOCUMENT_NODE | 9 | #document | null |
| Document Type Node | DOCUMENT_TYPE_NODE | 10 | Document type name | null |
| Document Fragment Node | DOCUMENT_FRAGMENT_NODE | 11 | #document-fragment | null |
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.
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 nodechildNodes 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:
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 siblingProperties with Element skip text and comment nodes and return element nodes directly, making them more convenient for daily use.
Relationship Properties Comparison
| Property | Returns All Node Types | Only Element Nodes |
|---|---|---|
| Parent node | parentNode | parentElement |
| Child nodes list | childNodes | children |
| First child node | firstChild | firstElementChild |
| Last child node | lastChild | lastElementChild |
| Previous sibling | previousSibling | previousElementSibling |
| Next sibling | nextSibling | nextElementSibling |
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
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:
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:
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 nodesNodeFilter.SHOW_ELEMENT: Element nodesNodeFilter.SHOW_TEXT: Text nodesNodeFilter.SHOW_COMMENT: Comment nodes
You can also pass a custom filter function for more fine-grained control over traversal behavior:
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
// 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 scriptsDocument Ready State
Before manipulating the DOM, you need to ensure the document has finished loading. document.readyState has three possible values:
- loading: Document is loading
- interactive: Document has been parsed, but resources (like images) are still loading
- complete: Document and all resources have finished loading
// 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
// ❌ 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
// ❌ 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 updateDocumentFragment 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:
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 4HTMLCollection can access elements by index or the item() method, and can also find elements by id or name attributes using the namedItem() method:
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:
// 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:
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:
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:
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:
- Accurately locate elements: Navigate the DOM tree using parent-child and sibling relationships
- Efficiently traverse nodes: Choose appropriate traversal methods based on needs
- Understand node types: Distinguish between different types like element nodes, text nodes, etc.
- 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.