DOM 是什麼?前端 DOM 操作完整教學
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
| 面向 | getElementById | querySelector |
|---|---|---|
| 選取方式 | 只能用 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 | 滑鼠移出元素 | 任何元素 |
DOMContentLoaded | DOM 解析完成 | 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 變數 的教學。