DOM編程全解析:操作、事件與存儲實戰指南

引言:DOM——JavaScript與網頁交互的橋梁

DOM(文檔對象模型) 是JavaScript操作HTML/XML文檔的接口,它將網頁文檔抽象為一個樹形結構,允許開發者通過API動態修改文檔的內容、結構和樣式。無論是實現動態交互(如按鈕點擊)、表單驗證,還是數據持久化(如本地存儲用戶偏好),DOM編程都是前端開發的核心基礎。

本文將系統講解DOM編程的三大核心模塊:DOM操作(元素創建、修改、刪除)、事件系統(事件綁定、事件流、事件對象)、表單與存儲(表單處理、驗證、Web Storage),每個知識點均配備可直接運行的示例代碼,幫助你從入門到精通DOM編程。

一、DOM基礎:理解文檔樹與節點類型

1.1 DOM樹結構與節點類型

DOM將HTML文檔表示為一棵節點樹,每個HTML元素、文本、屬性都是樹中的節點。常見節點類型包括:

節點類型描述示例
元素節點HTML標簽元素<div><p><input>
文本節點元素內的文本內容Hello World<p>內的文本)
屬性節點元素的屬性class="active"id="logo"
文檔節點整個文檔的根節點document對象
注釋節點HTML注釋<!-- 這是注釋 -->

示例:簡單DOM樹結構

<html><head><title>DOM示例</title></head><body><h1>歡迎學習DOM</h1><p class="content">這是一個段落</p></body>
</html>

對應的DOM樹:

document(文檔節點)
└── html(元素節點)├── head(元素節點)│   └── title(元素節點)│       └── "DOM示例"(文本節點)└── body(元素節點)├── h1(元素節點)│   └── "歡迎學習DOM"(文本節點)└── p(元素節點,屬性:class="content")└── "這是一個段落"(文本節點)

1.2 獲取DOM元素:從選擇到訪問

操作DOM的第一步是獲取元素,JavaScript提供了多種選擇器API,適用于不同場景:

1.2.1 基礎選擇器(直接獲取單個/多個元素)
// 1. 通過ID獲取(返回單個元素,推薦,性能最優)
const title = document.getElementById("main-title");// 2. 通過類名獲取(返回HTMLCollection,類數組,實時更新)
const items = document.getElementsByClassName("list-item");// 3. 通過標簽名獲取(返回HTMLCollection)
const paragraphs = document.getElementsByTagName("p");// 4. 通過name屬性獲取(返回NodeList,常用于表單元素)
const usernameInput = document.getElementsByName("username");
1.2.2 高級選擇器(CSS選擇器語法,靈活強大)
// 1. querySelector(返回第一個匹配元素,支持復雜CSS選擇器)
const firstItem = document.querySelector(".list-item:first-child");
const activeLink = document.querySelector("a.active");// 2. querySelectorAll(返回NodeList,非實時更新,支持forEach遍歷)
const allItems = document.querySelectorAll("ul li");
const inputs = document.querySelectorAll('input[type="text"]');// NodeList遍歷示例
allItems.forEach((item, index) => {console.log(`${index+1}個列表項:`, item.textContent);
});

注意HTMLCollection(如getElementsByClassName返回)是實時集合(DOM變化時自動更新),而NodeList(如querySelectorAll返回)默認非實時,但支持forEach遍歷,更推薦使用。

二、DOM操作:動態修改文檔結構與樣式

2.1 節點CRUD:創建、插入、刪除與替換

2.1.1 創建節點
// 創建元素節點
const newDiv = document.createElement("div");// 創建文本節點
const textNode = document.createTextNode("這是動態創建的文本");// 創建屬性節點(較少直接使用,通常通過setAttribute設置)
const classAttr = document.createAttribute("class");
classAttr.value = "dynamic-div";
newDiv.setAttributeNode(classAttr); // 等價于 newDiv.className = "dynamic-div"// 組合節點
newDiv.appendChild(textNode); // 將文本節點添加到div中
newDiv.innerHTML = "<p>通過innerHTML直接設置HTML內容</p>"; // 更簡潔的方式
2.1.2 插入節點
const container = document.getElementById("container");
const newP = document.createElement("p");
newP.textContent = "新段落";// 1. appendChild:添加到容器末尾
container.appendChild(newP);// 2. insertBefore:插入到指定節點之前(需指定參考節點)
const referenceNode = container.querySelector("p:first-child");
container.insertBefore(newP, referenceNode); // 插入到第一個p元素之前// 3. 現代API:prepend(添加到開頭)、append(添加到末尾,支持多個節點)
container.prepend(newP, document.createElement("hr")); // 開頭添加p和hr
2.1.3 刪除與替換節點
const oldNode = document.getElementById("old-node");
const newNode = document.createElement("span");
newNode.textContent = "替換后的內容";// 刪除節點(需通過父節點調用)
oldNode.parentNode.removeChild(oldNode);// 替換節點
oldNode.parentNode.replaceChild(newNode, oldNode);

2.2 修改元素屬性與樣式

2.2.1 屬性操作
const link = document.querySelector("a");// 獲取屬性
console.log(link.getAttribute("href")); // 獲取href值
console.log(link.href); // 直接訪問屬性(返回絕對路徑,與getAttribute不同)// 設置屬性
link.setAttribute("href", "https://example.com");
link.title = "示例鏈接"; // 直接賦值(更簡潔)// 移除屬性
link.removeAttribute("target");// 檢查屬性是否存在
console.log(link.hasAttribute("rel")); // true/false
2.2.2 樣式操作
const box = document.querySelector(".box");// 1. 內聯樣式(style屬性,優先級最高)
box.style.width = "200px"; // 注意:屬性名是駝峰式(如fontSize)
box.style.backgroundColor = "#f0f0f0"; // 等價于CSS的background-color
box.style.cssText = "width: 300px; height: 200px; border: 1px solid red;"; // 批量設置// 2. 類名操作(推薦,通過CSS類控制樣式,便于維護)
box.classList.add("active"); // 添加類
box.classList.remove("old-class"); // 移除類
box.classList.toggle("hidden"); // 切換類(存在則移除,不存在則添加)
box.classList.contains("active"); // 檢查類是否存在(返回true/false)// 3. 獲取計算樣式(最終應用的樣式,包含所有CSS來源)
const computedStyle = window.getComputedStyle(box);
console.log("計算后的寬度:", computedStyle.width);
console.log("字體大小:", computedStyle.fontSize);

2.3 批量操作優化:DocumentFragment

頻繁DOM操作會導致重排重繪(Reflow/Repaint),影響性能。使用DocumentFragment可以批量處理節點,減少DOM操作次數:

const list = document.getElementById("big-list");
const fragment = document.createDocumentFragment(); // 文檔片段,內存中臨時容器// 模擬批量創建100個列表項
for (let i = 0; i < 100; i++) {const li = document.createElement("li");li.textContent = `列表項 ${i+1}`;fragment.appendChild(li); // 先添加到fragment(不觸發重排)
}// 最后一次性添加到DOM(僅觸發一次重排)
list.appendChild(fragment);

三、事件系統:響應用戶交互的核心機制

3.1 事件流:捕獲→目標→冒泡

DOM事件流分為三個階段(IE8及以下僅支持冒泡階段):

  1. 捕獲階段:事件從window向下傳播到目標元素的父節點
  2. 目標階段:事件到達目標元素本身
  3. 冒泡階段:事件從目標元素向上傳播回window

示例:事件流演示

<div id="outer" style="padding: 20px; background: #f0f0f0;"><div id="inner" style="padding: 20px; background: #ccc;"><button id="btn">點擊我</button></div>
</div>
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
const btn = document.getElementById("btn");// 捕獲階段綁定(第三個參數為true)
outer.addEventListener("click", () => console.log("outer捕獲"), true);
inner.addEventListener("click", () => console.log("inner捕獲"), true);
btn.addEventListener("click", () => console.log("btn捕獲"), true);// 冒泡階段綁定(第三個參數為false或省略)
outer.addEventListener("click", () => console.log("outer冒泡"));
inner.addEventListener("click", () => console.log("inner冒泡"));
btn.addEventListener("click", () => console.log("btn冒泡"));

點擊按鈕后輸出順序

outer捕獲 → inner捕獲 → btn捕獲 → btn冒泡 → inner冒泡 → outer冒泡

3.2 事件綁定方式對比

3.2.1 內聯事件(HTML屬性,不推薦)
<!-- 直接在HTML中綁定,耦合度高,難以維護 -->
<button onclick="alert('點擊了!')">按鈕1</button>
<button onclick="handleClick()">按鈕2</button><script>function handleClick() {alert("通過函數綁定點擊");}
</script>
3.2.2 DOM屬性綁定(JavaScript,單事件綁定)
const btn = document.getElementById("btn");// 覆蓋式綁定(只能綁定一個事件處理函數)
btn.onclick = function() {console.log("點擊事件1");
};
btn.onclick = function() {console.log("點擊事件2"); // 覆蓋前一個事件處理函數
};// 移除事件
btn.onclick = null;
3.2.3 addEventListener(推薦,支持多事件綁定與事件流控制)
const btn = document.getElementById("btn");// 綁定多個事件處理函數
btn.addEventListener("click", function handler1() {console.log("點擊事件1");
});
btn.addEventListener("click", function handler2() {console.log("點擊事件2"); // 兩個事件都會執行
});// 移除事件(必須使用命名函數,匿名函數無法移除)
btn.removeEventListener("click", handler1);// 綁定捕獲階段事件
btn.addEventListener("click", () => console.log("捕獲階段事件"), true);

推薦使用addEventListener:支持綁定多個事件、控制事件流階段、靈活移除事件,是現代DOM事件綁定的標準方式。

3.3 事件委托:高效處理動態元素事件

事件委托利用事件冒泡機制,將子元素的事件統一綁定到父元素上,尤其適合動態生成的元素(如列表項、動態加載的內容)。

3.3.1 傳統方式的問題(動態元素無法綁定事件)
// 初始列表項可以綁定事件,但動態添加的無法觸發
document.querySelectorAll("ul li").forEach(li => {li.addEventListener("click", () => {console.log("點擊了列表項:", li.textContent);});
});// 動態添加新列表項(無法觸發點擊事件)
const newLi = document.createElement("li");
newLi.textContent = "新列表項";
document.querySelector("ul").appendChild(newLi);
3.3.2 事件委托解決方案
const ul = document.querySelector("ul");// 將事件綁定到父元素ul上
ul.addEventListener("click", (e) => {// 通過事件對象的target判斷是否點擊了liif (e.target.tagName === "LI") { // 注意tagName是大寫console.log("點擊了列表項:", e.target.textContent);// 可以通過closest進一步精確匹配(如帶特定類的li)const targetLi = e.target.closest("li.active");if (targetLi) {console.log("點擊了激活狀態的列表項");}}
});// 動態添加的li也能觸發事件
const newLi = document.createElement("li");
newLi.textContent = "新列表項(支持點擊)";
ul.appendChild(newLi);

優勢:減少事件綁定次數(只綁定父元素)、自動支持動態元素、優化性能(尤其對大量子元素)。

3.4 事件對象:獲取事件詳情與控制行為

事件處理函數的第一個參數為事件對象(Event),包含事件相關信息(如觸發元素、坐標、按鍵等),并提供控制事件行為的方法。

3.4.1 常用事件對象屬性
document.addEventListener("click", (e) => {console.log("觸發元素:", e.target); // 實際點擊的元素(可能是子元素)console.log("綁定事件的元素:", e.currentTarget); // 綁定事件的父元素(此處為document)console.log("事件類型:", e.type); // "click"console.log("鼠標坐標(相對于頁面):", e.pageX, e.pageY);console.log("是否按下Ctrl鍵:", e.ctrlKey);
});
3.4.2 常用事件對象方法
// 1. preventDefault:阻止默認行為(如鏈接跳轉、表單提交)
document.querySelector("a").addEventListener("click", (e) => {e.preventDefault(); // 阻止鏈接跳轉console.log("鏈接被點擊,但已阻止跳轉");
});// 2. stopPropagation:阻止事件冒泡(不再向上傳播)
document.getElementById("inner").addEventListener("click", (e) => {e.stopPropagation(); // 阻止冒泡到outerconsole.log("inner點擊,已阻止冒泡");
});// 3. stopImmediatePropagation:阻止冒泡+阻止同元素后續事件處理函數
btn.addEventListener("click", (e) => {e.stopImmediatePropagation();console.log("第一個點擊事件");
});
btn.addEventListener("click", () => {console.log("第二個點擊事件(不會執行)");
});

四、表單處理:獲取值、驗證與提交

4.1 表單元素值獲取

4.1.1 基礎表單元素
// 獲取表單元素
const form = document.getElementById("user-form");
const username = form.elements.username; // 通過form.elements獲取(推薦)
const password = document.querySelector('input[name="password"]');
const email = document.getElementById("email");// 獲取值
console.log("用戶名:", username.value);
console.log("密碼:", password.value);
console.log("郵箱:", email.value);
4.1.2 特殊表單元素(單選、多選、下拉框)
// 單選按鈕(獲取選中的值)
const genderRadios = document.querySelectorAll('input[name="gender"]');
const selectedGender = Array.from(genderRadios).find(radio => radio.checked)?.value;// 復選框(獲取所有選中的值)
const hobbiesCheckboxes = document.querySelectorAll('input[name="hobbies"]:checked');
const selectedHobbies = Array.from(hobbiesCheckboxes).map(checkbox => checkbox.value);// 下拉框(select元素)
const citySelect = document.getElementById("city");
const selectedCity = citySelect.value; // 單選下拉框
const selectedCities = Array.from(citySelect.selectedOptions).map(option => option.value); // 多選下拉框(需設置multiple)

4.2 表單驗證

4.2.1 HTML5原生驗證(簡單高效)
<form id="login-form"><div><label>用戶名:</label><input type="text" name="username" required  <!-- 必填 -->minlength="3"  <!-- 最小長度 -->maxlength="10"  <!-- 最大長度 -->pattern="^[a-zA-Z0-9_]+$"  <!-- 正則匹配(字母、數字、下劃線) -->title="用戶名只能包含字母、數字和下劃線,長度3-10"  <!-- 驗證失敗提示 -->></div><div><label>郵箱:</label><input type="email" name="email" required> <!-- 自動驗證郵箱格式 --></div><div><label>年齡:</label><input type="number" name="age" min="18" max="120" required> <!-- 數字范圍 --></div><button type="submit">提交</button>
</form>
4.2.2 自定義驗證(復雜邏輯)
const form = document.getElementById("login-form");// 自定義驗證函數
function validateUsername(username) {if (username.includes("admin")) {return { valid: false, message: "用戶名不能包含'admin'" };}return { valid: true };
}// 監聽表單提交事件
form.addEventListener("submit", (e) => {e.preventDefault(); // 阻止默認提交const username = form.username.value;const email = form.email.value;// 執行自定義驗證const usernameValid = validateUsername(username);if (!usernameValid.valid) {alert(usernameValid.message);return;}// 原生驗證API(checkValidity檢查單個元素,reportValidity顯示默認提示)if (!form.email.checkValidity()) {form.email.reportValidity(); // 觸發瀏覽器默認錯誤提示return;}// 驗證通過,提交表單(AJAX示例)fetch("/api/login", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify({ username, email })}).then(response => response.json()).then(data => console.log("登錄成功:", data)).catch(error => console.error("登錄失敗:", error));
});

五、Web存儲:持久化數據存儲方案

5.1 localStorage與sessionStorage對比

特性localStoragesessionStorage
存儲周期永久存儲,除非手動刪除僅當前會話(標簽頁關閉后清除)
作用域同源窗口共享僅當前標簽頁私有
存儲容量約5MB約5MB
APIsetItem/getItem/removeItem/clear同上

5.2 基礎API使用

5.2.1 localStorage示例(持久化存儲)
// 存儲數據(鍵值對,值只能是字符串)
localStorage.setItem("username", "Alice");
localStorage.setItem("isLogin", "true"); // 布爾值會被轉為字符串// 獲取數據
const username = localStorage.getItem("username");
const isLogin = localStorage.getItem("isLogin") === "true"; // 需手動轉換類型// 刪除數據
localStorage.removeItem("isLogin");// 清空所有數據(謹慎使用)
// localStorage.clear();
5.2.2 存儲復雜數據(JSON序列化)
// 存儲對象
const userInfo = { name: "Bob", age: 25, hobbies: ["reading", "coding"] };
localStorage.setItem("userInfo", JSON.stringify(userInfo)); // 序列化為JSON字符串// 獲取對象
const storedUser = JSON.parse(localStorage.getItem("userInfo")); // 反序列化為對象
console.log("用戶名:", storedUser.name);
console.log("年齡:", storedUser.age);// 存儲數組
const recentSearches = ["JavaScript", "DOM編程"];
localStorage.setItem("recentSearches", JSON.stringify(recentSearches));
5.2.3 sessionStorage示例(會話存儲)
// 僅在當前標簽頁有效,刷新頁面保留,關閉標簽頁丟失
sessionStorage.setItem("tempData", "這是臨時數據");// 跨標簽頁無法訪問
// 在新標簽頁執行 sessionStorage.getItem("tempData") → null

5.3 存儲限制與安全考量

  • 容量限制:單個域名下約5MB,超出會拋出QuotaExceededError
  • 安全風險:存儲內容明文可見,不要存儲敏感信息(如密碼、token)。
  • 存儲優化:定期清理過期數據,復雜數據使用IndexedDB(更大容量,支持查詢)。

六、綜合實戰:待辦事項列表(DOM+事件+存儲)

6.1 需求與HTML結構

實現一個待辦事項列表,支持添加、刪除、標記完成功能,并使用localStorage持久化數據。

<div class="todo-app"><h1>待辦事項列表</h1><form id="todo-form"><input type="text" id="todo-input" placeholder="輸入新任務..." required><button type="submit">添加</button></form><ul id="todo-list"></ul>
</div>

6.2 JavaScript實現

class TodoApp {constructor() {this.todoList = document.getElementById("todo-list");this.todoInput = document.getElementById("todo-input");this.form = document.getElementById("todo-form");this.todos = JSON.parse(localStorage.getItem("todos")) || []; // 從存儲加載數據this.init();}// 初始化:綁定事件、渲染列表init() {this.form.addEventListener("submit", (e) => this.handleAddTodo(e));this.todoList.addEventListener("click", (e) => this.handleTodoClick(e)); // 事件委托this.renderTodos();}// 渲染待辦列表renderTodos() {this.todoList.innerHTML = ""; // 清空列表this.todos.forEach((todo, index) => {const li = document.createElement("li");li.className = todo.completed ? "completed" : "";li.innerHTML = `<input type="checkbox" ${todo.completed ? "checked" : ""}><span>${todo.text}</span><button class="delete-btn">×</button>`;li.dataset.index = index; // 存儲索引,用于刪除this.todoList.appendChild(li);});// 保存到localStoragelocalStorage.setItem("todos", JSON.stringify(this.todos));}// 添加待辦事項handleAddTodo(e) {e.preventDefault();const text = this.todoInput.value.trim();if (!text) return;this.todos.push({ text, completed: false });this.todoInput.value = ""; // 清空輸入框this.renderTodos();}// 處理待辦項點擊(標記完成/刪除)handleTodoClick(e) {const index = e.target.closest("li").dataset.index;if (e.target.type === "checkbox") {// 標記完成/未完成this.todos[index].completed = e.target.checked;} else if (e.target.classList.contains("delete-btn")) {// 刪除待辦項this.todos.splice(index, 1);}this.renderTodos();}
}// 初始化應用
new TodoApp();

6.3 樣式補充(CSS)

.todo-app { max-width: 500px; margin: 20px auto; padding: 0 20px; }
#todo-list { list-style: none; padding: 0; }
#todo-list li { padding: 10px; margin: 5px 0; background: #f5f5f5; border-radius: 4px;display: flex; align-items: center; gap: 10px;
}
#todo-list li.completed span { text-decoration: line-through; color: #888; }
.delete-btn { margin-left: auto; background: #ff4444; color: white; border: none; border-radius: 50%; cursor: pointer; }

七、常見問題與最佳實踐

7.1 DOM操作性能優化

  • 減少重排重繪:批量修改DOM時使用DocumentFragment,避免頻繁修改style屬性(使用classList替代)。
  • 事件委托:對動態元素使用事件委托,減少事件綁定次數。
  • 緩存DOM查詢:避免在循環中重復查詢DOM(如for (let i=0; i<document.querySelectorAll('li').length; i++))。

7.2 事件綁定注意事項

  • 移除事件監聽:使用addEventListener綁定的事件,需通過removeEventListener移除(傳入相同函數),避免內存泄漏。
  • 阻止默認行為:表單提交、鏈接跳轉需顯式調用e.preventDefault()

7.3 存儲安全與限制

  • 敏感數據:禁止存儲密碼、token等,可使用HttpOnly Cookie或加密存儲。
  • 容量控制:定期清理無用數據,超出5MB時提供用戶提示。

八、總結與進階學習

DOM編程是前端開發的基石,本文涵蓋了DOM操作、事件系統、表單處理與Web存儲的核心知識點,并通過實戰案例展示了如何綜合應用。掌握這些技能后,你可以實現復雜的交互效果和數據持久化功能。

進階學習方向

  • DOM高級APIIntersectionObserver(滾動監聽)、MutationObserver(DOM變化監聽)
  • 虛擬DOM:理解React/Vue的虛擬DOM原理,減少真實DOM操作
  • Web Components:自定義元素,封裝可復用的DOM組件
  • IndexedDB:替代localStorage的大容量結構化存儲方案

通過持續實踐(如實現購物車、表單生成器),你將逐步提升DOM編程能力,為構建復雜前端應用打下堅實基礎!#### 4.1.2 復雜表單元素(單選框、復選框、下拉框)

// 單選框(name屬性相同的為一組)
const genderRadios = document.querySelectorAll('input[name="gender"]');
let selectedGender;
genderRadios.forEach(radio => {if (radio.checked) {selectedGender = radio.value; // 獲取選中值}
});// 復選框(獲取所有選中項)
const hobbies = Array.from(document.querySelectorAll('input[name="hobby"]:checked')).map(checkbox => checkbox.value); // 轉換為值數組// 下拉框(select)
const citySelect = document.getElementById("city");
const selectedCity = citySelect.value; // 單選下拉框
const selectedOptions = Array.from(citySelect.selectedOptions).map(option => option.value); // 多選下拉框(需設置multiple屬性)// 文件上傳(input[type="file"])
const fileInput = document.getElementById("avatar");
fileInput.addEventListener("change", (e) => {const file = e.target.files[0]; // 獲取第一個選中文件console.log("文件名:", file.name);console.log("文件大小:", file.size);console.log("文件類型:", file.type);
});

4.2 表單驗證:HTML5驗證與自定義驗證

4.2.1 HTML5內置驗證屬性
<form id="register-form"><!-- 必填項 --><input type="text" name="username" required placeholder="用戶名(必填)"><!-- 最小/最大長度 --><input type="password" name="password" minlength="6" maxlength="20" required><!-- 正則表達式驗證 --><input type="text" name="phone" pattern="^1[3-9]\d{9}$" placeholder="手機號"><!-- 數值范圍 --><input type="number" name="age" min="18" max="120" placeholder="年齡(18-120)"><!-- 郵箱格式驗證 --><input type="email" name="email" required placeholder="郵箱"><button type="submit">提交</button>
</form>
4.2.2 JavaScript觸發驗證
const form = document.getElementById("register-form");form.addEventListener("submit", (e) => {// 觸發表單驗證(若驗證不通過,會顯示默認錯誤提示)if (!form.checkValidity()) {e.preventDefault(); // 阻止表單提交console.log("表單驗證失敗");// 手動聚焦到第一個無效字段const invalidField = form.querySelector(":invalid");invalidField.focus();} else {e.preventDefault(); // 實際項目中通過AJAX提交console.log("表單驗證通過,準備提交");}
});
4.2.3 自定義驗證邏輯與錯誤提示
const password = document.getElementById("password");
const confirmPassword = document.getElementById("confirm-password");// 自定義驗證:密碼一致性檢查
confirmPassword.addEventListener("input", () => {if (password.value !== confirmPassword.value) {// 設置自定義錯誤消息confirmPassword.setCustomValidity("兩次密碼不一致");} else {confirmPassword.setCustomValidity(""); // 清除錯誤}
});// 自定義錯誤提示樣式(覆蓋瀏覽器默認提示)
form.addEventListener("invalid", (e) => {e.preventDefault(); // 阻止默認提示框const field = e.target;const errorMsg = field.validationMessage || "請輸入有效值";// 顯示自定義錯誤提示(假設頁面有.error-message元素)const errorElement = field.nextElementSibling;if (errorElement && errorElement.classList.contains("error-message")) {errorElement.textContent = errorMsg;}
}, true); // 捕獲階段觸發

五、Web存儲:localStorage、sessionStorage與Cookie

5.1 localStorage:持久化本地存儲

特點:永久存儲(除非手動刪除)、容量約5MB、同源策略限制、僅支持字符串存儲。

5.1.1 基本API
// 存儲數據(自動轉為字符串)
localStorage.setItem("username", "Alice");
localStorage.setItem("userAge", 25); // 存儲為"25"
localStorage.setItem("isVip", true); // 存儲為"true"// 獲取數據(需手動轉換類型)
const username = localStorage.getItem("username"); // "Alice"
const userAge = Number(localStorage.getItem("userAge")); // 25
const isVip = localStorage.getItem("isVip") === "true"; // true// 存儲對象(需JSON序列化)
const user = { name: "Bob", hobbies: ["reading", "coding"] };
localStorage.setItem("user", JSON.stringify(user));// 獲取對象(需JSON解析)
const savedUser = JSON.parse(localStorage.getItem("user"));
console.log(savedUser.hobbies[0]); // "reading"// 刪除數據
localStorage.removeItem("userAge");// 清空所有數據
// localStorage.clear();
5.1.2 監聽存儲變化(跨標簽頁通信)
window.addEventListener("storage", (e) => {console.log(`存儲鍵${e.key}發生變化`);console.log("舊值:", e.oldValue);console.log("新值:", e.newValue);console.log("存儲區域:", e.storageArea); // localStorage或sessionStorage
});

5.2 sessionStorage:會話級存儲

特點:僅在當前會話有效(標簽頁關閉后清除)、容量約5MB、同源策略限制、API與localStorage完全相同。

5.2.1 使用場景:臨時表單數據保存
// 監聽表單輸入,實時保存到sessionStorage
document.querySelectorAll("#temp-form input").forEach(input => {input.addEventListener("input", (e) => {const { name, value } = e.target;sessionStorage.setItem(`temp_${name}`, value);});
});// 頁面加載時恢復表單數據(如刷新頁面后)
window.addEventListener("load", () => {document.querySelectorAll("#temp-form input").forEach(input => {const savedValue = sessionStorage.getItem(`temp_${input.name}`);if (savedValue) {input.value = savedValue;}});
});

5.3 Cookie:小型文本存儲

特點:容量小(約4KB)、可設置有效期、可指定域名和路徑、每次請求自動攜帶。

5.3.1 Cookie操作函數
// 設置Cookie(name, value, daysToLive:有效期天數)
function setCookie(name, value, daysToLive = 7, path = "/", domain = "", secure = false) {let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;// 有效期if (daysToLive) {const date = new Date();date.setTime(date.getTime() + (daysToLive * 24 * 60 * 60 * 1000));cookie += `; expires=${date.toUTCString()}`;}// 路徑if (path) cookie += `; path=${path}`;// 域名if (domain) cookie += `; domain=${domain}`;// 安全標志(僅HTTPS)if (secure) cookie += `; secure`;document.cookie = cookie;
}// 獲取Cookie
function getCookie(name) {const cookies = document.cookie.split("; ");for (const cookie of cookies) {const [cookieName, cookieValue] = cookie.split("=");if (decodeURIComponent(cookieName) === name) {return decodeURIComponent(cookieValue);}}return null;
}// 刪除Cookie(設置過期時間為過去)
function deleteCookie(name, path = "/", domain = "") {setCookie(name, "", -1, path, domain);
}// 使用示例
setCookie("theme", "dark", 30); // 存儲30天
const theme = getCookie("theme"); // "dark"
deleteCookie("theme");
5.3.2 Cookie與Storage對比
特性localStoragesessionStorageCookie
存儲容量約5MB約5MB約4KB
有效期永久(手動刪除)會話期間可設置(默認會話)
網絡請求不發送到服務器不發送到服務器每次請求自動攜帶
訪問權限客戶端腳本客戶端腳本客戶端/服務器端
適用場景用戶偏好、持久數據臨時表單、頁面狀態身份認證、跟蹤

六、綜合實戰案例:待辦事項管理系統

6.1 功能需求

  • 添加待辦事項(支持回車提交)
  • 標記待辦事項為已完成/未完成
  • 刪除待辦事項
  • 數據持久化(localStorage保存)
  • 實時統計待辦/已完成數量

6.2 實現代碼

HTML結構
<div class="todo-app"><h1>待辦事項管理</h1><div class="stats"><span>待辦:<span id="todo-count">0</span></span><span>已完成:<span id="done-count">0</span></span></div><form id="todo-form"><input type="text" id="todo-input" placeholder="添加新的待辦事項..."><button type="submit">添加</button></form><ul id="todo-list" class="todo-list"></ul>
</div>
CSS樣式(簡化版)
.todo-app { max-width: 500px; margin: 20px auto; padding: 0 20px; }
.todo-list { list-style: none; padding: 0; }
.todo-item { padding: 10px; margin: 5px 0; border: 1px solid #ddd; display: flex; align-items: center; }
.todo-item.done .text { text-decoration: line-through; color: #999; }
.todo-item input[type="checkbox"] { margin-right: 10px; }
.todo-item .delete { margin-left: auto; cursor: pointer; color: red; }
.stats { margin-bottom: 10px; color: #666; }
JavaScript邏輯
document.addEventListener("DOMContentLoaded", () => {const form = document.getElementById("todo-form");const input = document.getElementById("todo-input");const todoList = document.getElementById("todo-list");const todoCountEl = document.getElementById("todo-count");const doneCountEl = document.getElementById("done-count");// 從localStorage加載待辦事項let todos = JSON.parse(localStorage.getItem("todos")) || [];// 渲染待辦事項列表function renderTodos() {todoList.innerHTML = ""; // 清空列表todos.forEach((todo, index) => {const li = document.createElement("li");li.className = `todo-item ${todo.done ? "done" : ""}`;li.innerHTML = `<input type="checkbox" ${todo.done ? "checked" : ""} data-index="${index}"><span class="text">${todo.text}</span><span class="delete" data-index="${index}">×</span>`;todoList.appendChild(li);});// 更新統計const todoCount = todos.filter(todo => !todo.done).length;const doneCount = todos.length - todoCount;todoCountEl.textContent = todoCount;doneCountEl.textContent = doneCount;// 保存到localStoragelocalStorage.setItem("todos", JSON.stringify(todos));}// 添加待辦事項form.addEventListener("submit", (e) => {e.preventDefault();const text = input.value.trim();if (text) {todos.push({ text, done: false });input.value = ""; // 清空輸入框renderTodos();}});// 回車提交input.addEventListener("keydown", (e) => {if (e.key === "Enter") form.dispatchEvent(new Event("submit"));});// 事件委托:處理復選框和刪除按鈕點擊todoList.addEventListener("click", (e) => {const index = e.target.dataset.index;if (index === undefined) return;if (e.target.type === "checkbox") {// 切換完成狀態todos[index].done = e.target.checked;} else if (e.target.classList.contains("delete")) {// 刪除待辦事項todos.splice(index, 1);}renderTodos();});// 初始化渲染renderTodos();
});

七、DOM編程性能優化與最佳實踐

7.1 DOM操作性能優化

7.1.1 減少重排重繪
  • 批量修改DOM:使用DocumentFragment或離線DOM樹
  • 避免頻繁讀取布局屬性:如offsetWidth/offsetHeight(會觸發重排),緩存計算結果
  • 使用CSS類批量修改樣式:避免多次設置element.style
  • 隱藏元素修改后再顯示element.style.display = "none"; 修改...; element.style.display = "block";
7.1.2 事件優化
  • 事件委托:減少事件綁定數量
  • 移除無用事件監聽:頁面卸載或元素刪除前解綁事件,避免內存泄漏
  • 防抖節流:處理高頻事件(resize、scroll、input)
// 防抖(一段時間內只執行最后一次)
function debounce(fn, delay = 300) {let timer;return (...args) => {clearTimeout(timer);timer = setTimeout(() => fn.apply(this, args), delay);};
}// 節流(一段時間內只執行一次)
function throttle(fn, interval = 300) {let lastTime = 0;return (...args) => {const now = Date.now();if (now - lastTime >= interval) {fn.apply(this, args);lastTime = now;}};
}// 使用示例(監聽窗口大小變化)
window.addEventListener("resize", debounce(() => {console.log("窗口大小變化(防抖)");
}, 500));

7.2 安全最佳實踐

  • 避免innerHTML插入不可信內容:防止XSS攻擊,優先使用textContent
  • 表單驗證:前后端雙重驗證,不信任前端輸入
  • 存儲敏感數據:避免在localStorage/Cookie存儲密碼等敏感信息,必要時加密

八、總結與進階學習

8.1 核心知識點回顧

  • DOM操作:元素選擇、內容修改、樣式操作、節點CRUD
  • 事件系統:事件流、委托、事件對象方法、高頻事件優化
  • 表單處理:值獲取、HTML5驗證、自定義驗證邏輯
  • Web存儲:localStorage/sessionStorage/Cookie的使用場景與限制

8.2 進階學習方向

  • DOM高級API:MutationObserver(監聽DOM變化)、IntersectionObserver(懶加載)
  • 虛擬DOM:理解React/Vue的虛擬DOM原理
  • Web Components:自定義元素、Shadow DOM
  • 性能監控:使用Chrome DevTools分析DOM性能瓶頸

8.3 推薦資源

  • 官方文檔:MDN DOM文檔

DOM編程是前端開發的基石,掌握本文內容后,你將能夠自如地實現網頁動態交互、表單處理和數據持久化。實踐是提升的關鍵,建議結合本文案例動手實現,并嘗試擴展功能(如待辦事項的分類、優先級設置),深化對DOM編程的理解。

補充:存儲方案詳細對比表格

特性localStoragesessionStorageCookie
存儲容量約5MB約5MB約4KB
有效期永久(需手動刪除)會話期間(標簽頁關閉后清除)可設置(默認會話)
網絡請求不發送到服務器不發送到服務器每次HTTP請求自動攜帶
訪問權限僅客戶端JavaScript僅客戶端JavaScript客戶端/服務器端均可訪問
API易用性簡潔(setItem/getItem等)同localStorage需手動解析document.cookie
數據類型僅字符串(需JSON序列化)同localStorage僅字符串
跨標簽頁共享支持(同源)不支持支持(符合路徑/域限制)
適用場景用戶偏好、持久化數據臨時表單數據、頁面狀態身份認證、會話跟蹤

補充:待辦事項系統編輯功能擴展

在原案例基礎上添加雙擊編輯功能:

// 事件委托:添加雙擊編輯功能
todoList.addEventListener("dblclick", (e) => {const index = e.target.closest(".todo-item").dataset.index;if (index === undefined) return;const todoItem = todos[index];const textSpan = e.target.closest(".text");if (textSpan) {// 創建輸入框替換文本const input = document.createElement("input");input.type = "text";input.value = todoItem.text;input.classList.add("edit-input");// 替換文本節點為輸入框textSpan.parentNode.replaceChild(input, textSpan);input.focus();// 失去焦點或回車保存const saveEdit = () => {const newText = input.value.trim();if (newText) {todos[index].text = newText;renderTodos();} else {// 空文本則刪除todos.splice(index, 1);renderTodos();}};input.addEventListener("blur", saveEdit);input.addEventListener("keydown", (e) => {if (e.key === "Enter") saveEdit();if (e.key === "Escape") renderTodos(); // 取消編輯});}
});

常見問題解答(FAQ)

Q1:querySelectorAll與getElementsByClassName的性能差異?

AgetElementsByClassName(實時HTMLCollection)在頻繁DOM操作場景性能較差(因實時更新),querySelectorAll(非實時NodeList)性能更穩定,推薦優先使用querySelectorAll

Q2:事件委托中如何精確匹配目標元素?

A:使用e.target.matches(selector)e.target.closest(selector)

ul.addEventListener("click", (e) => {if (e.target.matches("li.item")) { // 精確匹配li.itemconsole.log("點擊了列表項");}const target = e.target.closest(".todo-item"); // 查找最近的祖先元素if (target) { /* 處理邏輯 */ }
});
Q3:localStorage存儲對象時為什么需要JSON序列化?

A:因為localStorage僅支持字符串存儲,直接存儲對象會轉為"[object Object]",失去數據結構。使用JSON.stringify(obj)轉為JSON字符串,讀取時用JSON.parse()恢復對象。

Q4:如何監聽localStorage的變化?

A:通過window.addEventListener("storage", callback),但僅在同域下的其他標簽頁修改時觸發,當前標簽頁修改不會觸發。

Q5:表單提交時如何阻止默認行為并使用AJAX提交?

A

form.addEventListener("submit", (e) => {e.preventDefault(); // 阻止默認提交if (!form.checkValidity()) return; // 驗證表單// 收集表單數據const formData = new FormData(form);// 或手動收集:Object.fromEntries(new FormData(form))// AJAX提交(fetch示例)fetch("/api/submit", {method: "POST",body: formData}).then(res => res.json()).then(data => console.log("提交成功:", data)).catch(error => console.error("提交失敗:", error));
});

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/90193.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/90193.shtml
英文地址,請注明出處:http://en.pswp.cn/web/90193.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Ansible命令

Ansible命令 ansible 常用命令 /usr/bin/ansible   #Ansibe AD-Hoc 臨時命令執行工具&#xff0c;常用于臨時命令的執行 /usr/bin/ansible-doc    #Ansible 模塊功能查看工具 /usr/bin/ansible-galaxy   #下載/上傳優秀代碼或Roles模塊 的官網平臺&#xff0c;基于網…

SY6974芯片添加enable充電控制節點

1. 需求描述項目背景&#xff1a;基于 Qualcomm MDM9x07 平臺的 4G MIFI 產品&#xff0c;使用 Silergy 公司的 SY6974 充電 IC需求內容&#xff1a; 在環境 /sys/class/power_supply/sy6794/enable 下增加一個 sysfs 節點&#xff0c;用于控制是否允許充電&#xff1a;cat /sy…

趣玩-Ollama-Llm-Chatrbot

軟件說明 這個軟件本人是從零開始實現的聊天機器人。基于Ollama&#xff08;PythonApi &#xff09; Pyside&#xff0c;實現了聊天機器的基本功能&#xff0c;還有一些個性化的功能比如模型管理&#xff0c;敏感詞過濾&#xff0c;個性化主題設置&#xff0c;頭像設置等功能。…

在mac 上zsh 如何安裝最新的 go 工具

文章目錄方法一&#xff1a;使用 Homebrew&#xff08;推薦&#xff09;方法二&#xff1a;從官網下載安裝包方法三&#xff1a;使用 g&#xff08;Go 版本管理器&#xff09;方法四&#xff1a;使用 gvm&#xff08;Go Version Manager&#xff09;驗證安裝和配置常用 Go 工具…

(十九)深入了解 AVFoundation-編輯:使用 AVMutableVideoComposition 實現視頻加水印與圖層合成(上)——理論篇

一、引言在短視頻、Vlog、剪輯工具日益流行的今天&#xff0c;給視頻添加 Logo、水印、時間戳或動態貼紙&#xff0c;已經成為非常常見的功能需求。這類效果看似簡單&#xff0c;其實背后都涉及到“圖層合成”的處理&#xff1a;如何將一個靜態或動態的圖層&#xff08;如文字、…

Android NDK與JNI深度解析

核心概念定義&#xff1a;NDK (Native Development Kit): 是什么&#xff1a; 一套由 Google 提供的工具集合。目的&#xff1a; 允許 Android 開發者使用 C 和 C 等原生&#xff08;Native&#xff09;語言來實現應用程序的部分功能。包含內容&#xff1a; 交叉編譯器&#xf…

Golang各版本特性

1. Go各版本特性 | FeelingLife 2. https://chatgpt.com/share/68808f58-ae5c-800a-8153-5358098f301b 3.https://tonybai.com/2024/11/14/go-map-use-swiss-table/

HTML 轉 Word API 接口

HTML 轉 Word API 接口 支持網頁轉 Word&#xff0c;高效轉換為 Word&#xff0c;提供永久鏈接。 1. 產品功能 超高性能轉換效率&#xff1b;支持將傳遞的 HTML 轉換為 Word&#xff0c;支持 HTML 中的 CSS 格式在 Word 文檔中的呈現&#xff1b;支持傳遞網站的 URL&#xff…

Lucid Search: 極簡、隱私友好的問答式搜索引擎技術解析

Lucid Search: 極簡、隱私友好的問答式搜索引擎技術解析 產品定位與價值主張 Lucid Search 是一款革命性的問答式搜索引擎&#xff0c;其核心價值在于&#xff1a; 極簡體驗&#xff1a;無賬戶、無廣告、前端完全靜態隱私保護&#xff1a;不寫入 Cookie、不記錄 IP、無追蹤即…

卷積神經網絡:模型評估標準

一、分類模型評價指標在模型評估中&#xff0c;有多個標準用于衡量模型的性能&#xff0c;這些標準包括準確率&#xff08;Accuracy&#xff09;、精確率&#xff08;Precision&#xff09;、召回率&#xff08;Recall&#xff09;、F1 分數&#xff08;F1-Score&#xff09;等…

【前端工程化】前端開發中想做好發布管理可以從哪些方面著手?

在企業級后臺系統中&#xff0c;發布管理是整個開發流程的最終環節&#xff0c;也是最為關鍵的一環。它不僅涉及代碼構建完成后的部署操作&#xff0c;還包括版本控制、灰度發布、回滾機制等保障系統穩定性的措施。 本文主要圍繞發布流程設計、版本控制、部署方式、灰度策略和回…

替分布式=成本下降50% !

在數字化轉型的浪潮中&#xff0c;數據庫作為醫療信息系統的“心臟”&#xff0c;其穩定性與效率直接關乎醫療服務的質量。2024年10月30日&#xff0c;綿陽市第三人民醫院集成平臺的CDR數據庫成功從分布式數據庫Citus切換為國產集中式數據庫KingbaseES&#xff0c;并穩定運行至…

【Linux系統編程】基礎指令

基礎指令1. adduser指令&&passwd指令2. userdel指令3. pwd指令4. ls指令5. cd指令6. tree指令7. touch指令8. mkdir指令9. rmdir指令&&rm指令10. man指令11. cp指令12. mv指令13. cat指令14. more指令15. less指令16. head指令17. tail指令18. date指令19. cal…

區塊鏈之以太坊Hardhat開發框架——部署在windows為例

Hardhat 提供了一個靈活且易于使用的開發環境&#xff0c;可以輕松地編寫、測試和部署智能合約。Hardhat還內置了Hardhat 網絡&#xff08;Hardhat Node&#xff09;&#xff0c;它是為開發而設計的本地以太坊網絡。 下面是hardhat的官方文檔 https://hardhat.org/hardhat-ru…

Ubuntu 1804 編譯ffmpeg qsv MediaSDK libva 遇到的問題記錄

之前都是 用的xeon服務器的cpu 不支持intel QSV 硬件加速 最近把自己的 14年買的pc機裝上了ubuntu 1804 然后準備開啟ffmpeg qsv 硬件加速功能 CPU i3-4170 內存DDR3 16G 硬盤機械盤500G 主板ASUS B85M-G首先安裝vainfo工具apt install vainfo裝完提示如下出錯了 網上說是…

Elasticsearch(ES)介紹和安裝

目錄 一、Elasticsearch(ES)介紹 1.為什么需要單獨的搜索服務 2.全文檢索 3.Elasticsearch簡介 1.Elasticsearch的特點 2.應用場景 3.ElasticSearch數據的存儲和搜索原理 二、Elasticsearch(ES)安裝 1、拉取鏡像 2、創建目錄并給目錄賦權 3、創建并編輯配置文件 4、…

html結構解析

<!DOCTYPE html>&#xff1a;聲明為 HTML5 文檔 <html lang"zh-CN">&#xff1a;根元素&#xff0c;指定頁面語言為中文 <meta charset"UTF-8">&#xff1a;設置字符編碼&#xff0c;確保中文正常顯示 <meta name"viewport"…

面試150 最大子數組和

思路 貪心法&#xff1a;設定最小標志result為float(‘-inf’),遍歷一次數組元素進行求和&#xff0c;如果當前元素大于result&#xff0c;則更新result的值&#xff0c;如果sum小于0&#xff0c;則重新置0進行計算&#xff0c;最后返回result class Solution:def maxSubArray(…

MyBatis動態SQL實戰:告別硬編碼,擁抱智能SQL生成

MyBatis動態SQL實戰&#xff1a;告別硬編碼&#xff0c;擁抱智能SQL生成在電商平臺的用戶管理模塊中&#xff0c;需要面對多種不同的用戶查詢組合條件。當使用傳統的硬編碼SQL方式時&#xff0c;代碼膨脹到了2000多行&#xff0c;維護成本極高。而引入MyBatis動態SQL后&#xf…

Web前端開發:JavaScript遍歷方法詳解與對比

1. 傳統 for 循環const arr [10, 20, 30]; for (let i 0; i < arr.length; i) {console.log(索引 ${i}: 值 ${arr[i]}); } // 輸出&#xff1a; // 索引 0: 值 10 // 索引 1: 值 20 // 索引 2: 值 30特點&#xff1a;最基礎的循環&#xff0c;可通過索引精準控制適用場景&…