# 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)