DOM 是什麼?前端 DOM 操作完整教學

2023/12/30

DOM(Document Object Model,文件物件模型) 是瀏覽器將 HTML 文件解析後,在記憶體中建立的一個樹狀結構。透過 JavaScript 操作 DOM,我們可以動態地新增、修改、刪除頁面上的元素,實現各種互動效果,而無需重新載入整個頁面。

什麼是 DOM?

當瀏覽器載入一個 HTML 文件時,它不只是把 HTML 原始碼顯示出來,而是先將整個 HTML **解析(Parse)**成一個由節點(Node)組成的樹狀結構,這個結構就叫做 DOM 樹(DOM Tree)

你可以把 DOM 想像成一份家譜:<html> 是祖先,<head><body> 是它的子代,<body> 底下又有各自的子元素,層層相連。JavaScript 就是透過這份「家譜」來找到並操作任何一個節點。

DOM 不等於 HTML 原始碼。HTML 是靜態的文字,DOM 是瀏覽器解析後建立在記憶體中的動態物件模型。這就是為什麼你用 JavaScript 修改 DOM 後,頁面會即時更新,但原始的 HTML 檔案並不會改變。

DOM Tree 的結構

以以下 HTML 為例:

<!DOCTYPE html>
<html>
  <head>
    <title>我的頁面</title>
  </head>
  <body>
    <h1 id="title">Hello DOM</h1>
    <p class="intro">這是一段說明文字。</p>
  </body>
</html>

對應的 DOM 樹結構如下:

document
└── html
    ├── head
    │   └── title
    │       └── "我的頁面"(Text 節點)
    └── body
        ├── h1 [id="title"]
        │   └── "Hello DOM"(Text 節點)
        └── p [class="intro"]
            └── "這是一段說明文字。"(Text 節點)

DOM 中有四種主要的節點類型(Node Type):

節點類型說明範例
Element 節點HTML 標籤元素<div><p><h1>
Text 節點元素內的純文字內容"Hello DOM"
Attribute 節點元素的屬性(現代 DOM 中通常直接存取)id="title"
Comment 節點HTML 的註解<!-- 這是註解 -->

以下是一個互動範例,展示如何用 JavaScript 操作 DOM:

See the Pen What's the DOM(什麼是DOM)-Javascript by lenrich (@lenrich) on CodePen.


選取 DOM 元素

要操作 DOM 中的元素,第一步是「選取」到它。JavaScript 提供了多種選取方法:

常用選取方法

// 透過 ID 選取(回傳單一元素,找不到回傳 null)
const title = document.getElementById('title');

// 透過 CSS 選擇器選取第一個符合的元素
const intro = document.querySelector('.intro');
const firstBtn = document.querySelector('button');
const navLink = document.querySelector('nav a');

// 透過 CSS 選擇器選取所有符合的元素(回傳 NodeList)
const allItems = document.querySelectorAll('.list-item');
const allParas = document.querySelectorAll('p');

// 透過 Class 名稱選取(回傳 HTMLCollection)
const cards = document.getElementsByClassName('card');

// 透過標籤名稱選取(回傳 HTMLCollection)
const allDivs = document.getElementsByTagName('div');

遍歷 NodeList

querySelectorAll 回傳的是 NodeList,可以用 forEach 遍歷:

const items = document.querySelectorAll('.list-item');

// 使用 forEach 遍歷
items.forEach((item, index) => {
  console.log(`第 ${index + 1} 個元素:`, item.textContent);
});

// 也可以轉成陣列後使用 Array 方法
const itemArray = Array.from(items);
const texts = itemArray.map(item => item.textContent);

querySelector vs getElementById

面向getElementByIdquerySelector
選取方式只能用 ID支援任何 CSS 選擇器
速度稍快(直接走 ID 索引)稍慢(需解析 CSS 選擇器)
靈活性低(只能選單一 ID)高(可組合複雜選擇器)
回傳值元素 或 null元素 或 null
用途快速選取有唯一 ID 的元素需要靈活選取時

實務上,現代開發大多直接用 querySelector / querySelectorAll,因為靈活性更高,差異在效能上幾乎可以忽略不計。

更多選取方法請參考:DOM 元素選取教學


建立和新增元素

選取現有元素之外,我們也可以用 JavaScript 動態建立全新的 DOM 元素並插入頁面。

基本流程

// 1. 建立新元素
const newPara = document.createElement('p');

// 2. 設定內容
newPara.textContent = '這是動態新增的段落';

// 3. 加入到頁面中
const container = document.querySelector('#container');
container.appendChild(newPara);

createElement、textContent、innerHTML

const container = document.querySelector('#container');

// createElement:建立指定標籤的元素節點
const newDiv = document.createElement('div');
const newBtn = document.createElement('button');
const newImg = document.createElement('img');

// textContent:設定純文字內容(不會解析 HTML 標籤,較安全)
newBtn.textContent = '點我!';

// innerHTML:設定 HTML 字串內容(會解析標籤,注意 XSS 風險)
newDiv.innerHTML = '<strong>粗體文字</strong> 和一般文字';

// 設定屬性
newImg.src = '/img/logo.png';
newImg.alt = '網站 Logo';

appendChild 與 insertBefore

const list = document.querySelector('#my-list');
const firstItem = list.querySelector('li:first-child');

// appendChild:將元素加到父元素的最後面
const lastItem = document.createElement('li');
lastItem.textContent = '最後一項';
list.appendChild(lastItem);

// insertBefore:將新元素插入到指定元素之前
const newFirstItem = document.createElement('li');
newFirstItem.textContent = '第一項(新)';
list.insertBefore(newFirstItem, firstItem);

// 現代做法:使用 prepend、append、before、after(更直覺)
list.prepend(document.createElement('li'));   // 插入到最前面
list.append(document.createElement('li'));    // 插入到最後面

修改元素

選取到元素後,我們可以修改它的各種屬性、樣式和 Class。

textContent vs innerHTML vs innerText

這三個屬性都能讀取或設定元素的文字內容,但有重要差別:

屬性說明效能安全性
textContent純文字,不解析 HTML 標籤,包含隱藏元素安全(不會執行 HTML)
innerHTML解析 HTML 標籤,可設定複雜的 HTML 結構較慢有 XSS 風險
innerText純文字,考慮 CSS 樣式(隱藏元素的文字不包含),會觸發 reflow最慢安全
const el = document.querySelector('#demo');

// textContent:設定純文字,標籤會被當作文字顯示
el.textContent = '<strong>這不會變粗體</strong>';

// innerHTML:設定 HTML,標籤會被解析渲染
el.innerHTML = '<strong>這會變粗體</strong>';

// innerText:設定純文字,會考慮 CSS 的 visibility/display
el.innerText = '可見的文字';

setAttribute 修改屬性

const link = document.querySelector('a');
const input = document.querySelector('input');

// setAttribute:設定任何 HTML 屬性
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
input.setAttribute('disabled', '');

// getAttribute:讀取屬性值
const href = link.getAttribute('href');

// removeAttribute:移除屬性
input.removeAttribute('disabled');

// 常見屬性可直接存取(效能較好)
link.href = 'https://example.com';
input.value = '預設值';
input.disabled = true;

classList 操作 CSS Class

const box = document.querySelector('.box');

// classList.add:新增 Class
box.classList.add('active');
box.classList.add('highlight', 'bold'); // 可一次新增多個

// classList.remove:移除 Class
box.classList.remove('active');

// classList.toggle:切換 Class(有就移除,沒有就新增)
box.classList.toggle('active');

// classList.contains:判斷是否有某個 Class
if (box.classList.contains('active')) {
  console.log('元素目前是 active 狀態');
}

// classList.replace:替換 Class
box.classList.replace('active', 'inactive');

直接修改 element.style

const el = document.querySelector('#demo');

// 直接設定 inline style(注意:CSS 屬性名稱改用 camelCase)
el.style.color = 'red';
el.style.backgroundColor = '#f0f0f0'; // background-color → backgroundColor
el.style.fontSize = '18px';
el.style.display = 'none'; // 隱藏元素
el.style.display = 'block'; // 顯示元素

// 讀取實際的計算樣式(包含 CSS 檔案的樣式)
const computedStyle = window.getComputedStyle(el);
console.log(computedStyle.color);

刪除元素

有兩種主要方式可以從 DOM 中移除元素:

// 方法 1:element.remove()(現代方法,更簡潔)
const el = document.querySelector('#to-delete');
el.remove();

// 方法 2:parent.removeChild()(傳統方法,IE 相容)
const parent = document.querySelector('#container');
const child = document.querySelector('#child');
parent.removeChild(child);

// 實際應用:刪除清單中被點擊的項目
const listItems = document.querySelectorAll('#my-list li');
listItems.forEach(item => {
  item.addEventListener('click', function () {
    this.remove();
  });
});

事件監聽

DOM 事件讓網頁可以對使用者的行為做出回應。使用 addEventListener 來監聽各種事件:

const btn = document.querySelector('#my-btn');

// 基本語法:element.addEventListener(事件類型, 回呼函式)
btn.addEventListener('click', function (event) {
  console.log('按鈕被點擊了!');
  console.log('事件物件:', event);
  console.log('被點擊的元素:', event.target);
});

常見事件類型

事件類型觸發時機常用元素
click滑鼠點擊按鈕、連結、任何元素
input輸入框值改變時(即時)<input><textarea>
change輸入框值改變並離焦後<input><select>
submit表單提交<form>
keydown鍵盤按鍵按下任何可聚焦元素、document
keyup鍵盤按鍵放開任何可聚焦元素
scroll頁面或元素捲動window、可捲動元素
mouseover滑鼠移入元素任何元素
mouseout滑鼠移出元素任何元素
DOMContentLoadedDOM 解析完成document
load頁面所有資源載入完成window
// 表單提交事件(防止預設行為)
const form = document.querySelector('#contact-form');
form.addEventListener('submit', function (event) {
  event.preventDefault(); // 阻止頁面重新整理
  const name = document.querySelector('#name').value;
  console.log('提交的名稱:', name);
});

// 鍵盤事件
document.addEventListener('keydown', function (event) {
  console.log('按下的鍵:', event.key, '鍵碼:', event.code);
  if (event.key === 'Escape') {
    closeModal();
  }
});

// 即時輸入
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', function () {
  console.log('目前輸入值:', this.value);
  filterResults(this.value);
});

Event Delegation(事件委派)

當你有大量需要監聽的元素(例如一個清單有 100 個項目),逐一幫每個元素掛上事件監聽器會消耗大量記憶體,而且後來動態新增的元素不會自動套用。

Event Delegation(事件委派) 的做法是:只在父元素掛一個事件監聽器,利用事件冒泡(Event Bubbling)機制,當子元素被點擊時,事件會向上冒泡到父元素,再在父元素中判斷是哪個子元素觸發的。

// 不好的做法:幫每個 li 都綁定事件(效能差,動態新增的元素無效)
const items = document.querySelectorAll('#my-list li');
items.forEach(item => {
  item.addEventListener('click', handleClick);
});

// 好的做法:Event Delegation(只綁一個監聽器在父元素)
const list = document.querySelector('#my-list');
list.addEventListener('click', function (event) {
  // event.target 是實際被點擊的元素
  const clickedItem = event.target.closest('li');
  if (clickedItem) {
    console.log('點擊了:', clickedItem.textContent);
    clickedItem.classList.toggle('selected');
  }
});

// 動態新增的元素也能自動被監聽到
const newItem = document.createElement('li');
newItem.textContent = '動態新增的項目';
list.appendChild(newItem); // 點擊這個項目一樣有效!

常見問題(FAQ)

Q1:DOM 和 HTML 有什麼不同?

HTML 是靜態的標記語言文字,它是你寫在 .html 檔案裡的原始碼,本質上就是一段文字字串。

DOM 是瀏覽器載入 HTML 後,在記憶體中建立的動態物件模型。DOM 是活的,可以被 JavaScript 讀取和修改。

另一個差異:HTML 不一定是合法的 XML(例如可以省略結束標籤),但瀏覽器在解析時會自動補齊錯誤的 HTML,建立出完整的 DOM 樹。所以你看到的 DOM 結構不一定和你寫的 HTML 完全一致。

Q2:querySelector 和 getElementById 哪個比較快?

getElementById 理論上比 querySelector('#id') 稍快,因為它直接走瀏覽器內建的 ID 索引表。但在現代瀏覽器中,這個差異極其微小(通常是微秒等級),在實際應用中幾乎不影響效能。

除非你在一個超高頻率的迴圈中(例如每秒執行幾千次),否則不需要為了效能而放棄 querySelector 的靈活性。

如果需要選取的元素剛好有 ID,可以繼續用 getElementById;若選取條件較複雜,直接用 querySelector

了解更多 DOM 選取技巧:DOM 元素選取教學

Q3:使用 innerHTML 有什麼安全風險?

innerHTML 最大的風險是 XSS(Cross-Site Scripting,跨站腳本攻擊)。當你將使用者輸入的內容直接用 innerHTML 插入頁面,惡意使用者可以輸入 JavaScript 程式碼,讓它在其他使用者的瀏覽器中執行。

// 危險!使用者可能輸入 <script>document.cookie</script>
const userInput = getUserInput();
el.innerHTML = userInput; // 不要這樣做!

// 安全的做法:使用 textContent(純文字,不解析 HTML)
el.textContent = userInput;

// 或使用 DOMPurify 套件對 HTML 進行消毒(如果一定要用 innerHTML)
// npm install dompurify
el.innerHTML = DOMPurify.sanitize(userInput);

黃金原則:永遠不要把不受信任的內容(尤其是使用者輸入)直接塞入 innerHTML。如果只需要顯示文字,一律用 textContent

想了解更多 JavaScript 基礎知識,可以參考 JavaScript 變數 的教學。