JavaScript 基礎:DOM 操作

2026-01-26 07:59 | By justin | JavaScript
(Updated: 2026-01-26 07:59)

JavaScript 基礎:DOM 操作

各位好!

這篇是 JavaScript 系列的第十一單元,我們要學習 DOM(Document Object Model)操作。DOM 讓我們可以用 JavaScript 控制網頁的內容和樣式。


什麼是 DOM?

DOM 是瀏覽器提供的介面,將 HTML 轉換成樹狀結構,讓 JavaScript 可以存取和修改。

<!DOCTYPE html>
<html>
  <head>
    <title>範例</title>
  </head>
  <body>
    <div id="app">
      <h1>標題</h1>
      <p>段落</p>
    </div>
  </body>
</html>

<!-- DOM 樹結構:
Document
  └─ html
      ├─ head
      │   └─ title
      │       └─ "範例"
      └─ body
          └─ div#app
              ├─ h1
              │   └─ "標題"
              └─ p
                  └─ "段落"
-->

選取元素

document.getElementById()

// HTML: <div id="app">內容</div>

const app = document.getElementById("app");
console.log(app);  // <div id="app">內容</div>

// 如果找不到回傳 null
const notExist = document.getElementById("notExist");
console.log(notExist);  // null

document.querySelector()

使用 CSS 選擇器選取第一個符合的元素。

// HTML:
// <div class="container">
//   <p class="text">第一段</p>
//   <p class="text">第二段</p>
// </div>

// 選取第一個 .text
const firstText = document.querySelector(".text");
console.log(firstText.textContent);  // "第一段"

// 選取 id
const app = document.querySelector("#app");

// 複雜選擇器
const p = document.querySelector(".container > p.text");

// 屬性選擇器
const input = document.querySelector("input[type='email']");

// 偽類選擇器
const firstChild = document.querySelector("ul li:first-child");

document.querySelectorAll()

選取所有符合的元素,回傳 NodeList。

// 選取所有 .text
const allTexts = document.querySelectorAll(".text");
console.log(allTexts.length);  // 2

// NodeList 可以用 forEach
allTexts.forEach(text => {
  console.log(text.textContent);
});

// 或轉成陣列
const textsArray = Array.from(allTexts);
const textsArray2 = [...allTexts];

// 用陣列方法
const contents = [...allTexts].map(text => text.textContent);

其他選取方法

// getElementsByClassName - 回傳 HTMLCollection
const items = document.getElementsByClassName("item");

// getElementsByTagName - 回傳 HTMLCollection
const paragraphs = document.getElementsByTagName("p");

// getElementsByName - 通常用於表單
const radios = document.getElementsByName("gender");

// HTMLCollection 是「活的」,會自動更新
// NodeList(querySelectorAll)是「靜態的」

// 建議:優先使用 querySelector 和 querySelectorAll

修改內容

textContent

const heading = document.querySelector("h1");

// 讀取文字內容
console.log(heading.textContent);

// 設定文字內容
heading.textContent = "新標題";

// textContent 會將 HTML 標籤當作純文字
heading.textContent = "<strong>粗體</strong>";
// 顯示:<strong>粗體</strong>(不是粗體效果)

innerHTML

const container = document.querySelector(".container");

// 讀取 HTML 內容
console.log(container.innerHTML);

// 設定 HTML 內容
container.innerHTML = "<p>新段落</p>";

// 可以插入 HTML 標籤
container.innerHTML = "<strong>粗體</strong>";
// 顯示:粗體(有粗體效果)

// 小心:innerHTML 有 XSS 風險
const userInput = "<img src=x onerror='alert(1)'>";
container.innerHTML = userInput;  // 危險!會執行 JavaScript

// 安全做法:使用 textContent 或先清理輸入

innerText vs textContent

// HTML:
// <div id="test">
//   <p>可見文字</p>
//   <p style="display: none;">隱藏文字</p>
// </div>

const div = document.querySelector("#test");

// textContent:取得所有文字(包含隱藏的)
console.log(div.textContent);  // "可見文字隱藏文字"

// innerText:只取得可見的文字
console.log(div.innerText);  // "可見文字"

// 建議:使用 textContent(效能較好)

修改屬性

getAttribute / setAttribute

const link = document.querySelector("a");

// 取得屬性
const href = link.getAttribute("href");
console.log(href);

// 設定屬性
link.setAttribute("href", "https://example.com");
link.setAttribute("target", "_blank");

// 移除屬性
link.removeAttribute("target");

// 檢查屬性是否存在
if (link.hasAttribute("href")) {
  console.log("有 href 屬性");
}

直接存取屬性

const input = document.querySelector("input");

// 讀取
console.log(input.value);
console.log(input.type);
console.log(input.placeholder);

// 設定
input.value = "新值";
input.type = "password";
input.placeholder = "請輸入密碼";

// 布林屬性
input.disabled = true;
input.checked = true;
input.required = true;

// 常見屬性
const img = document.querySelector("img");
img.src = "image.jpg";
img.alt = "圖片說明";

const link = document.querySelector("a");
link.href = "https://example.com";
link.target = "_blank";

data 屬性

// HTML: <div id="user" data-id="123" data-role="admin">

const user = document.querySelector("#user");

// 使用 dataset
console.log(user.dataset.id);    // "123"
console.log(user.dataset.role);  // "admin"

// 設定
user.dataset.status = "active";
// 產生:<div data-status="active">

// 多字屬性(使用小駝峰)
user.dataset.firstName = "Alice";
// 產生:<div data-first-name="Alice">

// 刪除
delete user.dataset.status;

修改樣式

style 屬性

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

// 讀取行內樣式
console.log(box.style.color);

// 設定單一樣式
box.style.color = "red";
box.style.backgroundColor = "blue";  // CSS 的 background-color
box.style.fontSize = "20px";

// 一次設定多個樣式(使用 cssText)
box.style.cssText = "color: red; background-color: blue; font-size: 20px;";

// 移除樣式
box.style.color = "";

// 注意:style 只能存取行內樣式,無法取得 CSS 檔案的樣式

getComputedStyle()

// 取得計算後的樣式(包含 CSS 檔案的樣式)
const box = document.querySelector(".box");
const styles = getComputedStyle(box);

console.log(styles.color);
console.log(styles.backgroundColor);
console.log(styles.width);  // 實際寬度,如 "200px"

classList

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

// 新增 class
box.classList.add("active");
box.classList.add("highlight", "large");  // 一次加多個

// 移除 class
box.classList.remove("active");
box.classList.remove("highlight", "large");

// 切換 class(有就移除,沒有就加上)
box.classList.toggle("active");

// 檢查是否有某個 class
if (box.classList.contains("active")) {
  console.log("有 active class");
}

// 取代 class
box.classList.replace("old-class", "new-class");

// 取得所有 class
console.log(box.classList);  // DOMTokenList

// 轉成陣列
const classes = [...box.classList];

建立和刪除元素

建立元素

// 建立元素
const div = document.createElement("div");
const p = document.createElement("p");
const span = document.createElement("span");

// 設定內容
div.textContent = "這是一個 div";
p.innerHTML = "<strong>粗體文字</strong>";

// 設定屬性
div.id = "myDiv";
div.className = "container";
p.classList.add("text");

// 建立文字節點
const textNode = document.createTextNode("純文字");

插入元素

const container = document.querySelector(".container");
const newElement = document.createElement("p");
newElement.textContent = "新段落";

// appendChild - 加到最後
container.appendChild(newElement);

// insertBefore - 插入到指定元素前
const firstChild = container.firstElementChild;
container.insertBefore(newElement, firstChild);

// append - 可以加多個元素或文字
container.append(newElement, "純文字", anotherElement);

// prepend - 加到最前面
container.prepend(newElement);

// before / after - 插入到元素前後
const target = document.querySelector("#target");
target.before(newElement);  // 插入到 target 前面
target.after(newElement);   // 插入到 target 後面

// insertAdjacentHTML - 插入 HTML 字串
container.insertAdjacentHTML("beforebegin", "<p>前面</p>");
container.insertAdjacentHTML("afterbegin", "<p>開頭</p>");
container.insertAdjacentHTML("beforeend", "<p>結尾</p>");
container.insertAdjacentHTML("afterend", "<p>後面</p>");

刪除元素

const element = document.querySelector(".to-remove");

// remove - 移除元素本身
element.remove();

// removeChild - 移除子元素
const parent = document.querySelector(".parent");
const child = document.querySelector(".child");
parent.removeChild(child);

// 清空所有子元素
parent.innerHTML = "";
// 或
while (parent.firstChild) {
  parent.removeChild(parent.firstChild);
}

複製元素

const original = document.querySelector(".original");

// 淺複製(不包含子元素)
const shallowCopy = original.cloneNode(false);

// 深複製(包含所有子元素)
const deepCopy = original.cloneNode(true);

// 插入複製的元素
document.body.appendChild(deepCopy);

事件處理

addEventListener

const button = document.querySelector("button");

// 基本用法
button.addEventListener("click", function() {
  console.log("按鈕被點擊");
});

// 使用箭頭函式
button.addEventListener("click", () => {
  console.log("按鈕被點擊");
});

// 事件物件
button.addEventListener("click", (event) => {
  console.log(event.type);      // "click"
  console.log(event.target);    // 被點擊的元素
  console.log(event.currentTarget);  // 綁定事件的元素
});

// 移除事件監聽器
function handleClick() {
  console.log("點擊");
}

button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

// 注意:箭頭函式無法移除
button.addEventListener("click", () => {});
button.removeEventListener("click", () => {});  // 無效!

常見事件

// 滑鼠事件
element.addEventListener("click", () => {});      // 點擊
element.addEventListener("dblclick", () => {});   // 雙擊
element.addEventListener("mousedown", () => {});  // 按下
element.addEventListener("mouseup", () => {});    // 放開
element.addEventListener("mousemove", () => {});  // 移動
element.addEventListener("mouseenter", () => {}); // 進入
element.addEventListener("mouseleave", () => {}); // 離開

// 鍵盤事件
input.addEventListener("keydown", (e) => {
  console.log(e.key);     // 按下的鍵
  console.log(e.code);    // 鍵盤代碼
  console.log(e.ctrlKey); // 是否按 Ctrl
  console.log(e.shiftKey); // 是否按 Shift
});
input.addEventListener("keyup", () => {});
input.addEventListener("keypress", () => {});  // 已廢棄

// 表單事件
form.addEventListener("submit", (e) => {
  e.preventDefault();  // 阻止表單提交
});
input.addEventListener("input", () => {});   // 輸入中
input.addEventListener("change", () => {});  // 值改變
input.addEventListener("focus", () => {});   // 取得焦點
input.addEventListener("blur", () => {});    // 失去焦點

// 其他事件
window.addEventListener("load", () => {});      // 頁面載入完成
window.addEventListener("resize", () => {});    // 視窗大小改變
window.addEventListener("scroll", () => {});    // 捲動
document.addEventListener("DOMContentLoaded", () => {});  // DOM 載入完成

事件委派(Event Delegation)

// 不好:為每個子元素綁定事件
const items = document.querySelectorAll(".item");
items.forEach(item => {
  item.addEventListener("click", () => {
    console.log("點擊項目");
  });
});

// 好:使用事件委派
const list = document.querySelector(".list");
list.addEventListener("click", (event) => {
  // 檢查被點擊的元素
  if (event.target.classList.contains("item")) {
    console.log("點擊項目");
  }
});

// 實用範例:動態新增的元素也能觸發事件
const container = document.querySelector(".container");

container.addEventListener("click", (event) => {
  if (event.target.classList.contains("delete-btn")) {
    event.target.closest(".item").remove();
  }
});

// 之後新增的 .delete-btn 也會有效
const newItem = document.createElement("div");
newItem.innerHTML = '<button class="delete-btn">刪除</button>';
container.appendChild(newItem);

阻止預設行為和冒泡

// 阻止預設行為
const link = document.querySelector("a");
link.addEventListener("click", (event) => {
  event.preventDefault();  // 不跳轉連結
  console.log("連結被點擊,但不跳轉");
});

form.addEventListener("submit", (event) => {
  event.preventDefault();  // 不提交表單
  // 自訂處理邏輯
});

// 阻止事件冒泡
const parent = document.querySelector(".parent");
const child = document.querySelector(".child");

parent.addEventListener("click", () => {
  console.log("父元素被點擊");
});

child.addEventListener("click", (event) => {
  event.stopPropagation();  // 阻止冒泡到父元素
  console.log("子元素被點擊");
});

// 點擊 child 只會輸出「子元素被點擊」

表單處理

取得表單值

// HTML:
// <form id="myForm">
//   <input type="text" name="username">
//   <input type="email" name="email">
//   <input type="checkbox" name="agree">
//   <button type="submit">送出</button>
// </form>

const form = document.querySelector("#myForm");

form.addEventListener("submit", (event) => {
  event.preventDefault();

  // 方法一:直接存取
  const username = form.querySelector('[name="username"]').value;
  const email = form.querySelector('[name="email"]').value;
  const agree = form.querySelector('[name="agree"]').checked;

  console.log({ username, email, agree });

  // 方法二:使用 FormData
  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  console.log(data);

  // FormData 的方法
  console.log(formData.get("username"));
  console.log(formData.has("email"));
  formData.set("username", "新值");
  formData.delete("agree");

  // 遍歷
  for (const [key, value] of formData) {
    console.log(`${key}: ${value}`);
  }
});

表單驗證

const form = document.querySelector("#myForm");
const usernameInput = form.querySelector('[name="username"]');
const emailInput = form.querySelector('[name="email"]');

form.addEventListener("submit", (event) => {
  event.preventDefault();

  let isValid = true;

  // 驗證使用者名稱
  if (usernameInput.value.trim() === "") {
    showError(usernameInput, "使用者名稱不能為空");
    isValid = false;
  } else {
    clearError(usernameInput);
  }

  // 驗證 email
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(emailInput.value)) {
    showError(emailInput, "請輸入有效的 email");
    isValid = false;
  } else {
    clearError(emailInput);
  }

  if (isValid) {
    console.log("表單驗證通過");
    // 送出資料
  }
});

function showError(input, message) {
  const parent = input.parentElement;
  let error = parent.querySelector(".error");

  if (!error) {
    error = document.createElement("span");
    error.className = "error";
    parent.appendChild(error);
  }

  error.textContent = message;
  input.classList.add("invalid");
}

function clearError(input) {
  const parent = input.parentElement;
  const error = parent.querySelector(".error");

  if (error) {
    error.remove();
  }

  input.classList.remove("invalid");
}

實戰專案

待辦清單

const form = document.querySelector("#todoForm");
const input = document.querySelector("#todoInput");
const list = document.querySelector("#todoList");

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const text = input.value.trim();
  if (text === "") return;

  addTodo(text);
  input.value = "";
});

function addTodo(text) {
  const li = document.createElement("li");
  li.innerHTML = `
    <span class="todo-text">${text}</span>
    <button class="delete-btn">刪除</button>
  `;

  // 完成切換
  li.querySelector(".todo-text").addEventListener("click", () => {
    li.classList.toggle("completed");
  });

  // 刪除
  li.querySelector(".delete-btn").addEventListener("click", () => {
    li.remove();
  });

  list.appendChild(li);
}

// HTML:
// <form id="todoForm">
//   <input id="todoInput" type="text" placeholder="新增待辦事項">
//   <button type="submit">新增</button>
// </form>
// <ul id="todoList"></ul>

// CSS:
// .completed .todo-text { text-decoration: line-through; }

圖片輪播

const slides = document.querySelectorAll(".slide");
const prevBtn = document.querySelector(".prev");
const nextBtn = document.querySelector(".next");
let currentIndex = 0;

function showSlide(index) {
  slides.forEach((slide, i) => {
    slide.classList.toggle("active", i === index);
  });
}

function nextSlide() {
  currentIndex = (currentIndex + 1) % slides.length;
  showSlide(currentIndex);
}

function prevSlide() {
  currentIndex = (currentIndex - 1 + slides.length) % slides.length;
  showSlide(currentIndex);
}

nextBtn.addEventListener("click", nextSlide);
prevBtn.addEventListener("click", prevSlide);

// 自動輪播
let autoPlay = setInterval(nextSlide, 3000);

// 滑鼠移入暫停
document.querySelector(".slider").addEventListener("mouseenter", () => {
  clearInterval(autoPlay);
});

document.querySelector(".slider").addEventListener("mouseleave", () => {
  autoPlay = setInterval(nextSlide, 3000);
});

// 初始顯示
showSlide(0);

模態框(Modal)

const openBtn = document.querySelector("#openModal");
const closeBtn = document.querySelector("#closeModal");
const modal = document.querySelector(".modal");
const overlay = document.querySelector(".overlay");

function openModal() {
  modal.classList.add("active");
  overlay.classList.add("active");
  document.body.style.overflow = "hidden";
}

function closeModal() {
  modal.classList.remove("active");
  overlay.classList.remove("active");
  document.body.style.overflow = "";
}

openBtn.addEventListener("click", openModal);
closeBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", closeModal);

// ESC 鍵關閉
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && modal.classList.contains("active")) {
    closeModal();
  }
});

小結

這篇我們學習了:

  • 選取 DOM 元素的各種方法
  • 修改元素的內容、屬性、樣式
  • 建立、插入、刪除元素
  • 事件處理與事件委派
  • 表單操作與驗證
  • 實用的 DOM 專案

下一步:

完成這篇後,你已經能夠: - 操作網頁上的任何元素 - 處理使用者互動 - 建立動態網頁應用

前往下一篇:單元十二:ES6+ 現代語法總整理

在那裡,我們會回顧所有現代 JavaScript 語法。


0 留言

目前沒有留言

發表留言
回覆