
概念概覽
graph TD document["document"] --> html["html"] html --> head["head"] html --> body["body"] head --> title["title"] title --> titleText["'標題'"] body --> div["div"] div --> h1["h1"] div --> p["p"] h1 --> h1Text["'Hello'"] p --> pText["'World'"] style document fill:#f96,stroke:#333,stroke-width:2px style html fill:#fbb,stroke:#333 style head fill:#bfb,stroke:#333 style body fill:#bfb,stroke:#333 style titleText fill:#ffe,stroke:#999 style h1Text fill:#ffe,stroke:#999 style pText fill:#ffe,stroke:#999
什麼是 DOM?
每次你用 JavaScript 改變畫面上的文字、新增一個按鈕、或是監聽使用者的點擊,你其實都在操作 DOM。可以這樣說:HTML 是你寫的設計圖,DOM 是瀏覽器根據設計圖蓋出來的房子——而 JavaScript 就是你用來改裝這棟房子的工具。理解 DOM,是從「會寫 HTML」跨到「會寫互動式網頁」的關鍵一步。
DOM(Document Object Model,文件物件模型)是 W3C 定義的標準,它將 HTML/XML 文件表示為樹狀結構,讓程式語言可以存取和操作文件內容。
document
│
<html>
/ \
<head> <body>
│ │
<title> <div>
│ / \
"標題" <h1> <p>
│ │
"Hello" "World"
DOM 節點類型
| 節點類型 | nodeType | 說明 | 範例 |
|---|---|---|---|
| Element | 1 | HTML 元素 | <div>, <p> |
| Text | 3 | 文字內容 | ”Hello World” |
| Comment | 8 | 註解 | <!-- 註解 --> |
| Document | 9 | 整個文件 | document |
| DocumentFragment | 11 | 輕量容器 | createDocumentFragment() |
選取元素
基本選取方法
// 根據 ID 選取(最快)
const header = document.getElementById('header');
// 根據 class 選取(返回 HTMLCollection)
const items = document.getElementsByClassName('item');
// 根據標籤選取(返回 HTMLCollection)
const paragraphs = document.getElementsByTagName('p');
// CSS 選擇器(推薦)
const element = document.querySelector('.container > .item');
const elements = document.querySelectorAll('[data-active="true"]');HTMLCollection vs NodeList
// HTMLCollection - 即時更新(live)
const divs = document.getElementsByTagName('div');
// 新增 div 後,divs.length 會自動更新
// NodeList - 靜態快照(static)
const items = document.querySelectorAll('.item');
// 新增元素後,items.length 不變
// 轉換為陣列
const array1 = Array.from(divs);
const array2 = [...items];選到元素之後,接下來自然就是「對它做點什麼」對吧?DOM 操作的流程基本上就是:選取 → 操作 → 回應事件。我們先看怎麼建立和修改元素。
操作元素
建立與插入
// 建立元素
const div = document.createElement('div');
div.id = 'new-div';
div.className = 'container';
div.textContent = 'Hello';
// 插入方式
parent.appendChild(div); // 加到最後
parent.insertBefore(div, target); // 插入到 target 之前
parent.prepend(div); // 加到最前
parent.append(div); // 加到最後(可加多個)
// 現代方法(更靈活)
target.insertAdjacentElement('beforebegin', div); // target 前面
target.insertAdjacentElement('afterbegin', div); // target 內部最前
target.insertAdjacentElement('beforeend', div); // target 內部最後
target.insertAdjacentElement('afterend', div); // target 後面
// 插入 HTML 字串
element.insertAdjacentHTML('beforeend', '<span>新增</span>');修改與刪除
// 修改內容
element.textContent = '純文字'; // 只設文字
element.innerHTML = '<b>HTML</b>'; // 可設 HTML(注意 XSS)
// 修改屬性
element.setAttribute('data-id', '123');
element.getAttribute('data-id');
element.removeAttribute('data-id');
element.dataset.id = '123'; // data-* 屬性
// 修改樣式
element.style.backgroundColor = 'red';
element.style.cssText = 'color: blue; font-size: 16px;';
// 修改 class
element.classList.add('active');
element.classList.remove('active');
element.classList.toggle('active');
element.classList.contains('active');
// 刪除元素
element.remove(); // 現代方法
parent.removeChild(element); // 傳統方法
// 取代元素
parent.replaceChild(newElement, oldElement);
oldElement.replaceWith(newElement); // 現代方法光是能選取和修改元素還不夠——你的網頁需要回應使用者的操作。按鈕被點了、表單被送出了、滑鼠移到某個地方了…這些全靠事件處理來搞定。
事件處理
事件監聽
// 現代方法(推薦)
element.addEventListener('click', handleClick);
element.addEventListener('click', handleClick, { once: true }); // 只執行一次
// 移除監聽
element.removeEventListener('click', handleClick);
// 事件物件
element.addEventListener('click', (event) => {
event.target; // 觸發事件的元素
event.currentTarget; // 綁定事件的元素
event.preventDefault(); // 阻止預設行為
event.stopPropagation(); // 停止冒泡
});事件冒泡與捕獲
捕獲階段 冒泡階段
↓ ↑
┌──────────────────────────────────┐
│ document │
│ ┌────────────────────────────┐ │
│ │ body │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ div │ │ │
│ │ │ ┌────────────────┐ │ │ │
│ │ │ │ button ←點擊 │ │ │ │
│ │ │ └────────────────┘ │ │ │
│ │ └──────────────────────┘ │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
// 預設是冒泡階段
element.addEventListener('click', handler);
// 捕獲階段
element.addEventListener('click', handler, true);
element.addEventListener('click', handler, { capture: true });事件委派
// ❌ 為每個按鈕綁定事件
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', handleClick);
});
// ✅ 事件委派 - 綁定在父元素
document.querySelector('.btn-container').addEventListener('click', (e) => {
if (e.target.matches('.btn')) {
handleClick(e);
}
});效能優化
減少重排重繪
// ❌ 多次操作 DOM
for (let i = 0; i < 1000; i++) {
list.innerHTML += `<li>Item ${i}</li>`; // 每次都重排
}
// ✅ 使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // 只重排一次
// ✅ 或使用陣列 + innerHTML
const items = Array.from({ length: 1000 }, (_, i) => `<li>Item ${i}</li>`);
list.innerHTML = items.join('');讀寫分離
// ❌ 交錯讀寫,造成強制重排
elements.forEach(el => {
const height = el.offsetHeight; // 讀取
el.style.height = height + 10 + 'px'; // 寫入
});
// ✅ 先讀後寫
const heights = elements.map(el => el.offsetHeight); // 批次讀取
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // 批次寫入
});常用 DOM API
// 元素尺寸與位置
element.offsetWidth; // 包含 border
element.clientWidth; // 不含 border
element.scrollWidth; // 包含溢出內容
element.getBoundingClientRect(); // 相對於視窗的位置
// 滾動
element.scrollTop;
element.scrollTo({ top: 100, behavior: 'smooth' });
element.scrollIntoView({ behavior: 'smooth' });
// 焦點
element.focus();
element.blur();
document.activeElement; // 當前焦點元素