cover

概念概覽

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說明範例
Element1HTML 元素<div>, <p>
Text3文字內容”Hello World”
Comment8註解<!-- 註解 -->
Document9整個文件document
DocumentFragment11輕量容器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;  // 當前焦點元素

延伸閱讀

MDN - Document Object Model BOM 瀏覽器機制 JavaScript