---
# System prepended metadata

title: JavaScript DOM 操作
tags: [已重新整理]

---

# JavaScript DOM 操作

<!-- ## 目錄
- [DOM 選擇器](#DOM-選擇器)
- [DOM 事件](#DOM事件)
- [DOM Scroll 事件](#DOM-Scroll-事件)
- [DOM 建立與移除元素](#DOM-建立與移除元素)
- [DOM 修改與設置屬性](#DOM-修改與設置屬性)
- [DOM 節點查找](#DOM-節點查找)
- [表單操作](#表單操作)
- [實用範例](#實用範例)
- [效能優化建議](#效能優化建議)
- [常見問題](#常見問題)
--- -->

## DOM 選擇器

### 單一元素選擇

```javascript
// 選擇 ID
document.getElementById('myId')

// 選擇第一個符合的元素
document.querySelector('#myId')           // ID
document.querySelector('.myClass')        // Class
document.querySelector('div')             // 標籤
document.querySelector('[data-id="123"]') // 屬性
```

### 多個元素選擇

```javascript
// 選擇多個元素 (返回 HTMLCollection)
document.getElementsByTagName('div')
// 👉 回傳 HTMLCollection(3) [div, div, div]
document.getElementsByClassName('myClass')
// 👉 回傳 HTMLCollection(2) [div.myClass, p.myClass]

// 選擇多個元素 (返回 NodeList)
document.querySelectorAll('div')
// 👉 回傳 NodeList(3) [div, div, div]
document.querySelectorAll('.myClass')
// 👉 回傳 NodeList(3) [div.myClass, span.myClass, p.myClass]
```

**比較:**

| 方法 | 返回類型 | 是否即時更新 | 支援 CSS 選擇器 |
|------|---------|-------------|----------------|
| `getElementsBy...` | HTMLCollection | ✅ 是 | ❌ 否 |
| `querySelectorAll` | NodeList | ❌ 否 | ✅ 是 |

---

## DOM事件

### DOM 0 vs DOM 2 事件綁定

**DOM 0 (舊式):**
- 語法: `obj.onclick = function() {...}`
- 同一事件只能綁定一個處理函數,後者會覆蓋前者

**DOM 2 (推薦):**
- 語法: `obj.addEventListener('click', function() {...})`
- 同一事件可綁定多個處理函數,依序執行
- 可以移除事件監聽器

---

### 常用事件列表

#### 頁面載入事件

```javascript
// DOM 結構載入完成 (不等待圖片、CSS)
document.addEventListener('DOMContentLoaded', function() {
  console.log('DOM 已準備好')
})

// 頁面完全載入 (包含圖片、CSS)
window.addEventListener('load', function() {
  console.log('頁面完全載入')
})
```

#### 滑鼠事件

```javascript
const element = document.querySelector('.box')

// 點擊
element.addEventListener('click', function() {})

// 雙擊
element.addEventListener('dblclick', function() {})

// 按下滑鼠
element.addEventListener('mousedown', function() {})

// 放開滑鼠
element.addEventListener('mouseup', function() {})

// 滑鼠移動
element.addEventListener('mousemove', function() {})

// 滑鼠移入 (不包含子元素)
element.addEventListener('mouseenter', function() {})

// 滑鼠移入 (包含子元素)
element.addEventListener('mouseover', function() {})

// 滑鼠移出 (不包含子元素)
element.addEventListener('mouseleave', function() {})

// 滑鼠移出 (包含子元素)
element.addEventListener('mouseout', function() {})
```

**mouseenter vs mouseover 差異:**
- `mouseenter`: 只在進入元素本身時觸發
- `mouseover`: 進入元素或其子元素都會觸發

#### 鍵盤事件

```javascript
// 按下鍵盤
element.addEventListener('keydown', function(e) {
  console.log('按下:', e.key)
})

// 放開鍵盤
element.addEventListener('keyup', function(e) {
  console.log('放開:', e.key)
})
```

#### 表單事件

```javascript
const input = document.querySelector('input')

// 值改變時 (失去焦點後)
input.addEventListener('change', function() {})

// 值改變時 (即時)
input.addEventListener('input', function() {})

// 獲得焦點
input.addEventListener('focus', function() {})

// 失去焦點
input.addEventListener('blur', function() {})
```

#### 視窗事件

```javascript
// 視窗大小改變
window.addEventListener('resize', function() {
  console.log('視窗寬度:', window.innerWidth)
})

// 滾動事件
window.addEventListener('scroll', function() {
  console.log('滾動位置:', window.scrollY)
})
```

---

### 事件進階操作

#### 阻止預設行為

```javascript
// 阻止連結跳轉
link.addEventListener('click', function(event) {
  event.preventDefault()
})

// 阻止表單提交
form.addEventListener('submit', function(event) {
  event.preventDefault()
})
```

#### 停止事件冒泡

```javascript
element.addEventListener('click', function(e) {
  e.stopPropagation() // 防止事件向上傳遞
})
```

#### 事件委派 (動態綁定)

```javascript
// 適用於動態新增的元素
document.addEventListener('click', function(e) {
  // 判斷點擊的元素是否有特定 class
  if (e.target.classList.contains('btn')) {
    console.log('按鈕被點擊')
  }
})
```

---

## DOM Scroll 事件

### 滾動相關屬性

```javascript
// 頁面滾動位置
window.scrollY  // 垂直滾動距離
window.scrollX  // 水平滾動距離

// 視窗可視高度
window.innerHeight  // 相當於 100vh
window.innerWidth   // 視窗寬度
```

### 元素尺寸屬性

| 屬性 | 說明 | 包含內容 |
|------|------|---------|
| `offsetWidth` / `offsetHeight` | 元素可見寬高 | content + padding + border + scrollbar |
| `clientWidth` / `clientHeight` | 元素內部寬高 | content + padding |
| `scrollWidth` / `scrollHeight` | 元素完整寬高 | content + padding + 溢出內容 |

**圖解:**
![DOM_Scroll](https://hackmd.io/_uploads/SkUL3MQ0lx.jpg)


### 元素位置屬性

```javascript
const element = document.querySelector('.box')

// 相對於父元素的距離 (父元素需設定 position 為 relative/absolute)
element.offsetTop   // 上方距離
element.offsetLeft  // 左方距離

// 邊框寬度
element.clientTop   // 上邊框寬度
element.clientLeft  // 左邊框寬度

// 滾動距離
element.scrollTop   // 元素內容向上滾動的距離
element.scrollLeft  // 元素內容向左滾動的距離
```

### 取得元素位置資訊

```javascript
const rect = element.getBoundingClientRect()

console.log(rect.top)     // 元素頂部距離視窗頂部
console.log(rect.left)    // 元素左側距離視窗左側
console.log(rect.bottom)  // 元素底部距離視窗頂部
console.log(rect.right)   // 元素右側距離視窗左側
console.log(rect.width)   // 元素寬度
console.log(rect.height)  // 元素高度
```

### 滾動到指定位置

```javascript
// 滾動到座標
window.scrollTo(0, 500)

// 平滑滾動
window.scrollTo({
  top: 500,
  behavior: 'smooth'
})

// 滾動到元素位置
element.scrollIntoView({ behavior: 'smooth' })
```

### 滑鼠位置

```javascript
document.addEventListener('mousemove', function(e) {
  console.log('相對於視窗:', e.clientX, e.clientY)
  console.log('相對於頁面:', e.pageX, e.pageY)
  console.log('相對於元素:', e.offsetX, e.offsetY)
})
```

---

## DOM 建立與移除元素

### 建立元素

```javascript
// 建立元素節點
const div = document.createElement('div')

// 建立文字節點
const text = document.createTextNode('Hello World')
```

### 新增元素

```javascript
const parent = document.querySelector('.parent')
const newElement = document.createElement('div')

// 新增到最後
parent.appendChild(newElement)    // 返回新增的節點
parent.append(newElement)         // 無返回值,可接受多個參數

// 新增到最前
parent.prepend(newElement)

// 插入到特定位置
parent.insertAdjacentHTML('beforebegin', '<div>元素之前</div>')
parent.insertAdjacentHTML('afterbegin', '<div>第一個子元素之前</div>')
parent.insertAdjacentHTML('beforeend', '<div>最後一個子元素之後</div>')
parent.insertAdjacentHTML('afterend', '<div>元素之後</div>')
```

**insertAdjacentHTML 位置示意:**
```html
<!-- beforebegin -->
<div class="parent">
  <!-- afterbegin -->
  <p>子元素</p>
  <!-- beforeend -->
</div>
<!-- afterend -->
```

### 移除元素

```javascript
// 移除自己
element.remove()

// 移除子元素
parent.removeChild(child)

// 清空所有子元素
parent.innerHTML = ''
```

---

## DOM 修改與設置屬性

### 取得/設定內容

```javascript
const element = document.querySelector('.box')

// 純文字內容 (保留空白)
element.textContent = 'Hello'
console.log(element.textContent)

// 純文字內容 (過濾多餘空白)
element.innerText = 'Hello'
console.log(element.innerText)

// HTML 內容 (內部)
element.innerHTML = '<span>Hello</span>'
console.log(element.innerHTML)

// HTML 內容 (包含元素本身)
element.outerHTML = '<div class="new">Hello</div>'
console.log(element.outerHTML)
```

**差異比較:**

| 屬性 | 讀取內容 | 設定內容 | 效能 | 常見用途 |
|------|---------|---------|------|---------|
| `textContent` | 所有文字(含隱藏) | 純文字 | ⚡ 快 | 純文字操作 |
| `innerText` | 可見文字 | 純文字 | 🐢 慢 | 顯示文字 |
| `innerHTML` | HTML 標籤 | HTML | ⚡ 中 | 插入 HTML |
| `outerHTML` | 含元素本身 | 替換元素 | ⚡ 中 | 替換整個元素 |

### 屬性操作

```javascript
// 取得屬性
element.getAttribute('data-id')
element.id              // 直接存取
element.className       // 取得 class 字串
element.classList       // 取得 class 列表

// 設定屬性
element.setAttribute('data-id', '123')
element.id = 'myId'

// 移除屬性
element.removeAttribute('data-id')

// 判斷是否有屬性
element.hasAttribute('data-id')  // 返回 boolean
```

### Class 操作

```javascript
// 取得 class
element.className           // 字串格式
element.classList          // DOMTokenList 格式

// 新增 class
element.classList.add('active')
element.classList.add('class1', 'class2')  // 多個

// 移除 class
element.classList.remove('active')

// 切換 class (有就移除,沒有就新增)
element.classList.toggle('active')

// 判斷是否有特定 class
element.classList.contains('active')  // 返回 boolean

// 替換 class
element.classList.replace('old', 'new')
```

### 樣式操作

```javascript
// 設定單一樣式
element.style.color = 'red'
element.style.backgroundColor = 'blue'  // 駝峰式命名

// 設定多個樣式
element.style.cssText = 'color: red; background: blue;'

// 設定樣式 (含權重)
element.style.setProperty('color', 'red', 'important')

// 取得樣式
element.style.color

// 取得計算後的樣式 (包含 CSS 檔案的樣式)
const styles = window.getComputedStyle(element)
console.log(styles.color)
```

### 常用屬性快捷操作

```javascript
const img = document.querySelector('img')

// 圖片
img.src = 'image.jpg'
img.alt = '圖片說明'

// 連結
link.href = 'https://example.com'
link.target = '_blank'

// 標題
element.title = '提示文字'
```

---

## DOM 節點查找

### 向上查找 (父層)

```javascript
// 找到最近的父元素
element.parentElement

// 找到符合選擇器的最近父元素
element.closest('.parent')  // 往上找到第一個符合的元素
```

### 向下查找 (子層)

```javascript
// 找所有子元素 (只找第一層)
element.children  // 返回 HTMLCollection

// 找第一個子元素
element.firstElementChild

// 找最後一個子元素
element.lastElementChild

// 用選擇器找 (找所有子孫層)
element.querySelector('.child')
element.querySelectorAll('.child')
```

### 橫向查找 (兄弟元素)

```javascript
// 前一個兄弟元素
element.previousElementSibling

// 後一個兄弟元素
element.nextElementSibling
```

**節點關係圖:**
```
               parentElement
                     ↑
                     |
previousSibling ← element → nextSibling
                     |
                     ↓
                  children
```

---

## 表單操作

### 取得表單值

#### 文字輸入框

```javascript
const input = document.querySelector('input[type="text"]')

// 取得值
console.log(input.value)

// 設定值
input.value = 'Hello'
```

#### 文字區域

```javascript
const textarea = document.querySelector('textarea')

// 取得值
console.log(textarea.value)

// 設定值 (換行使用 \n)
textarea.value = '第一行\n第二行'
```

#### 下拉選單

```javascript
const select = document.querySelector('select')

// 取得選中的值
console.log(select.value)

// 設定選中項 (透過 value)
select.value = 'option2'

// 取得選中的文字
const selectedOption = select.options[select.selectedIndex]
console.log(selectedOption.text)
```

#### 單選框 (Radio)

```javascript
// 取得選中的值
const selected = document.querySelector('input[name="gender"]:checked')
console.log(selected ? selected.value : null)

// 設定選中
const radio = document.querySelector('input[value="male"]')
radio.checked = true
```

#### 複選框 (Checkbox)

```javascript
// 取得所有選中的值
const checkboxes = document.querySelectorAll('input[name="hobby"]:checked')
const values = Array.from(checkboxes).map(cb => cb.value)
console.log(values)

// 設定勾選
const checkbox = document.querySelector('input[value="reading"]')
checkbox.checked = true

// 判斷是否勾選
console.log(checkbox.checked)  // true 或 false
```

### 表單驗證

```javascript
const form = document.querySelector('form')

form.addEventListener('submit', function(e) {
  e.preventDefault()  // 阻止提交
  
  const input = document.querySelector('input[name="email"]')
  
  // 檢查是否為空
  if (!input.value.trim()) {
    alert('請輸入 Email')
    return
  }
  
  // Email 驗證
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!emailPattern.test(input.value)) {
    alert('Email 格式錯誤')
    return
  }
  
  // 通過驗證,可以提交
  console.log('驗證通過')
})
```

### 表單狀態控制

```javascript
const input = document.querySelector('input')

// 禁用
input.disabled = true

// 啟用
input.disabled = false

// 唯讀
input.readOnly = true

// 必填
input.required = true
```

---

## 實用範例

### 範例 1: 動態新增待辦事項

```javascript
const input = document.querySelector('#todo-input')
const btn = document.querySelector('#add-btn')
const list = document.querySelector('#todo-list')

btn.addEventListener('click', function() {
  if (!input.value.trim()) return
  
  // 建立新項目
  const li = document.createElement('li')
  li.textContent = input.value
  
  // 建立刪除按鈕
  const deleteBtn = document.createElement('button')
  deleteBtn.textContent = '刪除'
  deleteBtn.addEventListener('click', function() {
    li.remove()
  })
  
  li.appendChild(deleteBtn)
  list.appendChild(li)
  
  // 清空輸入框
  input.value = ''
})
```

### 範例 2: 滾動到頂部按鈕

```javascript
const scrollBtn = document.querySelector('#scroll-to-top')

// 滾動時顯示/隱藏按鈕
window.addEventListener('scroll', function() {
  if (window.scrollY > 300) {
    scrollBtn.style.display = 'block'
  } else {
    scrollBtn.style.display = 'none'
  }
})

// 點擊回到頂部
scrollBtn.addEventListener('click', function() {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  })
})
```

### 範例 3: 圖片懶加載

```javascript
const images = document.querySelectorAll('img[data-src]')

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src
      img.removeAttribute('data-src')
      imageObserver.unobserve(img)
    }
  })
})

images.forEach(img => imageObserver.observe(img))
```

---

## 效能優化建議

1. **減少 DOM 操作次數**
   ```javascript
   // ❌ 不好 - 多次操作 DOM
   for (let i = 0; i < 1000; i++) {
     list.innerHTML += `<li>${i}</li>`
   }
   
   // ✅ 好 - 一次操作
   let html = ''
   for (let i = 0; i < 1000; i++) {
     html += `<li>${i}</li>`
   }
   list.innerHTML = html
   ```

2. **使用事件委派**
   ```javascript
   // ❌ 不好 - 為每個按鈕綁定事件
   buttons.forEach(btn => {
     btn.addEventListener('click', handler)
   })
   
   // ✅ 好 - 只綁定一次
   container.addEventListener('click', function(e) {
     if (e.target.matches('button')) {
       handler(e)
     }
   })
   ```

3. **快取 DOM 查詢**
   ```javascript
   // ❌ 不好 - 重複查詢
   document.querySelector('.box').style.color = 'red'
   document.querySelector('.box').style.background = 'blue'
   
   // ✅ 好 - 快取元素
   const box = document.querySelector('.box')
   box.style.color = 'red'
   box.style.background = 'blue'
   ```

---

## 常見問題

### Q: `querySelector` vs `getElementById` 哪個快?

A: `getElementById` 較快,但差異很小。現代開發建議統一使用 `querySelector` 系列方法,因為更靈活。

### Q: 什麼時候用 `innerHTML` vs `textContent`?

A: 
- 插入 HTML 標籤時用 `innerHTML`
- 插入純文字時用 `textContent` (更安全,防止 XSS 攻擊)

### Q: `addEventListener` 可以重複綁定嗎?

A: 可以,同一事件可以綁定多個不同的處理函數,會依序執行。

---

## 參考資源

- [MDN - DOM 文檔](https://developer.mozilla.org/zh-TW/docs/Web/API/Document_Object_Model)
- [MDN - Event 參考](https://developer.mozilla.org/zh-TW/docs/Web/Events)
- [JavaScript.info - DOM](https://javascript.info/document)