owned this note
owned this note
Published
Linked with GitHub
# 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 + 溢出內容 |
**圖解:**

### 元素位置屬性
```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)