
HTML 是設計圖,DOM 是瀏覽器根據設計圖蓋出來的房子,JavaScript 是你改裝房子的工具。
先講結論
每次你用 JavaScript 改變畫面文字、新增按鈕、監聽點擊,你都在操作 DOM。DOM(Document Object Model)是 W3C 定義的標準,把 HTML 文件變成一棵樹狀結構,讓程式可以存取和修改網頁內容。理解 DOM,是從「會寫 HTML」跨到「會寫互動式網頁」的分水嶺。
document
│
<html>
/ \
<head> <body>
│ │
<title> <div>
│ / \
"標題" <h1> <p>
│ │
"Hello" "World"
選取元素:先找到你要改的東西
// CSS 選擇器(最推薦,彈性最大)
const el = document.querySelector('.container > .item');
const els = document.querySelectorAll('[data-active="true"]');
// 根據 ID(最快,但只能選一個)
const header = document.getElementById('header');一個容易踩坑的地方:getElementsByClassName 回傳的是 live 的 HTMLCollection——你動態新增元素,它的長度會自動更新。querySelectorAll 回傳的是 static 的 NodeList——是拿到那一瞬間的快照。搞混的話,你的 for loop 可能會跑出你意想不到的結果。
// HTMLCollection(live)- 新增 div 後 length 會變
const divs = document.getElementsByTagName('div');
// NodeList(static)- 新增元素後 length 不變
const items = document.querySelectorAll('.item');操作元素:建立、修改、刪除
選到元素之後,就是對它動手腳了。
// 建立並插入
const div = document.createElement('div');
div.textContent = 'Hello';
div.classList.add('card');
parent.appendChild(div);
// 更靈活的插入位置
target.insertAdjacentHTML('beforeend', '<span>新增的內容</span>');
// 修改屬性跟樣式
element.dataset.id = '123'; // data-* 屬性
element.classList.toggle('active'); // class 切換
element.style.transform = 'scale(1.1)'; // 行內樣式
// 刪除
element.remove();innerHTML 可以直接塞 HTML 字串,方便但有 XSS 風險。如果是使用者輸入的內容,用 textContent 比較安全。
事件處理:回應使用者的操作
光是能改元素還不夠——你的網頁需要回應使用者。
// 綁定事件
element.addEventListener('click', (event) => {
event.target; // 觸發事件的元素
event.currentTarget; // 綁定事件的元素
event.preventDefault(); // 阻止預設行為(例如表單送出)
event.stopPropagation(); // 停止冒泡
});事件冒泡是一個很重要的概念:點擊一個 button,事件會從 button → div → body → document 一路往上傳。這不是 bug,是 feature。因為有冒泡,我們才能用事件委派——把事件綁在父元素上,不用每個子元素都綁一次:
// ❌ 100 個按鈕綁 100 次
buttons.forEach(btn => btn.addEventListener('click', handler));
// ✅ 父元素綁一次,靠 event.target 判斷
container.addEventListener('click', (e) => {
if (e.target.matches('.btn')) handler(e);
});尤其當你的清單是動態新增的(比如 infinite scroll),事件委派幾乎是唯一合理的做法。
效能:別讓 DOM 操作拖垮你
DOM 操作是出了名的慢——不是 JavaScript 慢,是每次改 DOM 都可能觸發重排(Reflow)和重繪(Repaint)。
// ❌ 迴圈裡不斷改 innerHTML,每次都重排
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);另一個常見的坑是讀寫交錯。每次讀取 offsetHeight 之類的屬性,瀏覽器會強制重排來確保你拿到的是最新值。如果你在迴圈裡「讀一個、改一個、讀一個、改一個」,效能會非常慘。解法是先批次讀完,再批次寫入。
說到這裡你可能會想:「所以 React 的 Virtual DOM 就是為了解決這個問題?」沒錯,Virtual DOM 的核心價值就是把零散的 DOM 操作收集起來,算出最小差異後一次更新。但那是框架幫你做的事,用原生 JS 的話就得自己操心了。
DOM 操作是前端的基本功,框架再怎麼封裝,底層跑的還是這些東西。