JavaScript修改HTML DOM实战教程掌握动态网页开发的核心技能通过实际案例学习如何操作元素处理事件和更新页面内容
引言
在现代Web开发中,JavaScript操作DOM(文档对象模型)是创建动态、交互式网页的核心技能。DOM是一个编程接口,允许JavaScript访问和修改HTML文档的内容、结构和样式。通过掌握DOM操作,开发者可以创建响应式用户界面、处理用户交互、动态更新页面内容,从而提升用户体验。本教程将通过实际案例,系统地介绍如何使用JavaScript操作DOM元素、处理事件和更新页面内容。
DOM基础
什么是DOM
DOM(Document Object Model)是HTML和XML文档的编程接口。它将文档表示为一个树状结构,其中每个节点都是文档中的一个对象(如元素、属性、文本等)。JavaScript可以通过DOM API访问和操作这些对象。
<!DOCTYPE html> <html> <head> <title>DOM示例</title> </head> <body> <h1>欢迎学习DOM</h1> <p>这是一个段落。</p> </body> </html>
在上面的HTML文档中,DOM树结构如下:
Document └── html ├── head │ └── title │ └── "DOM示例" └── body ├── h1 │ └── "欢迎学习DOM" └── p └── "这是一个段落。"
DOM的重要性
DOM操作的重要性体现在:
- 动态内容更新:无需刷新页面即可更新内容
- 用户交互响应:实时响应用户的操作
- 创建丰富的用户体验:实现动画、拖放等复杂交互
- 数据驱动的界面:根据数据动态渲染界面
选择DOM元素
在操作DOM之前,首先需要选择要操作的元素。JavaScript提供了多种选择元素的方法。
getElementById
通过元素的ID选择单个元素。
<div id="myDiv">这是一个div元素</div> <script> // 通过ID选择元素 const myDiv = document.getElementById('myDiv'); console.log(myDiv.textContent); // 输出: 这是一个div元素 </script>
getElementsByClassName
通过类名选择多个元素,返回一个HTMLCollection。
<div class="box">盒子1</div> <div class="box">盒子2</div> <div class="box">盒子3</div> <script> // 通过类名选择元素 const boxes = document.getElementsByClassName('box'); console.log(boxes.length); // 输出: 3 // 遍历所有盒子 for (let i = 0; i < boxes.length; i++) { console.log(boxes[i].textContent); } </script>
getElementsByTagName
通过标签名选择多个元素,返回一个HTMLCollection。
<p>段落1</p> <p>段落2</p> <p>段落3</p> <script> // 通过标签名选择元素 const paragraphs = document.getElementsByTagName('p'); console.log(paragraphs.length); // 输出: 3 </script>
querySelector和querySelectorAll
使用CSS选择器语法选择元素。querySelector返回第一个匹配的元素,querySelectorAll返回所有匹配的元素(NodeList)。
<div id="container"> <p class="text">第一段</p> <p class="text">第二段</p> <div class="text">这是一个div</div> </div> <script> // 使用querySelector选择第一个匹配的元素 const firstText = document.querySelector('#container .text'); console.log(firstText.textContent); // 输出: 第一段 // 使用querySelectorAll选择所有匹配的元素 const allTexts = document.querySelectorAll('#container .text'); console.log(allTexts.length); // 输出: 3 // 遍历所有元素 allTexts.forEach(element => { console.log(element.textContent); }); </script>
修改元素内容
修改文本内容
使用textContent
或innerText
属性修改元素的文本内容。
<div id="textElement">原始文本</div> <button id="changeTextBtn">修改文本</button> <script> const textElement = document.getElementById('textElement'); const changeTextBtn = document.getElementById('changeTextBtn'); changeTextBtn.addEventListener('click', function() { // 使用textContent修改文本 textElement.textContent = '文本已被修改'; // 3秒后再次修改 setTimeout(() => { // 使用innerText修改文本(与textContent类似,但会考虑CSS样式) textElement.innerText = '再次修改的文本'; }, 3000); }); </script>
修改HTML内容
使用innerHTML
属性修改元素的HTML内容。
<div id="htmlElement"> <p>原始HTML内容</p> </div> <button id="changeHtmlBtn">修改HTML</button> <script> const htmlElement = document.getElementById('htmlElement'); const changeHtmlBtn = document.getElementById('changeHtmlBtn'); changeHtmlBtn.addEventListener('click', function() { // 使用innerHTML修改HTML内容 htmlElement.innerHTML = ` <ul> <li>列表项1</li> <li>列表项2</li> <li>列表项3</li> </ul> `; }); </script>
注意:使用innerHTML
存在安全风险,如果内容来自用户输入,可能导致XSS攻击。对于不受信任的内容,应使用textContent
或其他安全方法。
修改元素属性
标准属性
可以直接通过点表示法或setAttribute
方法修改元素的标准属性。
<img id="myImage" src="image1.jpg" alt="图片1" width="200"> <button id="changeAttrBtn">修改属性</button> <script> const myImage = document.getElementById('myImage'); const changeAttrBtn = document.getElementById('changeAttrBtn'); changeAttrBtn.addEventListener('click', function() { // 使用点表示法修改属性 myImage.src = 'image2.jpg'; myImage.alt = '图片2'; // 使用setAttribute方法修改属性 myImage.setAttribute('width', '300'); }); </script>
自定义属性(data-*)
HTML5允许使用data-*
属性存储自定义数据。
<div id="dataElement" data-user-id="123" data-role="admin">用户信息</div> <button id="showDataBtn">显示数据</button> <button id="changeDataBtn">修改数据</button> <script> const dataElement = document.getElementById('dataElement'); const showDataBtn = document.getElementById('showDataBtn'); const changeDataBtn = document.getElementById('changeDataBtn'); showDataBtn.addEventListener('click', function() { // 使用dataset属性访问data-*属性 const userId = dataElement.dataset.userId; const role = dataElement.dataset.role; alert(`用户ID: ${userId}, 角色: ${role}`); }); changeDataBtn.addEventListener('click', function() { // 修改data-*属性 dataElement.dataset.userId = '456'; dataElement.dataset.role = 'user'; alert('数据已修改'); }); </script>
修改元素样式
直接修改style属性
可以通过元素的style
对象直接修改CSS样式。
<div id="styleElement" style="width: 100px; height: 100px; background-color: blue;"></div> <button id="changeStyleBtn">修改样式</button> <script> const styleElement = document.getElementById('styleElement'); const changeStyleBtn = document.getElementById('changeStyleBtn'); changeStyleBtn.addEventListener('click', function() { // 直接修改style属性 styleElement.style.backgroundColor = 'red'; styleElement.style.width = '200px'; styleElement.style.height = '200px'; styleElement.style.borderRadius = '50%'; }); </script>
注意:CSS属性名在JavaScript中通常使用驼峰命名法,例如backgroundColor
代替background-color
。
通过classList修改类名
使用classList
属性添加、删除、切换或检查元素的类名。
<style> .box { width: 100px; height: 100px; background-color: blue; transition: all 0.3s ease; } .highlight { background-color: yellow; border: 2px solid red; } .rounded { border-radius: 50%; } </style> <div id="classElement" class="box"></div> <button id="addClassBtn">添加高亮</button> <button id="toggleClassBtn">切换圆角</button> <button id="removeClassBtn">移除高亮</button> <script> const classElement = document.getElementById('classElement'); const addClassBtn = document.getElementById('addClassBtn'); const toggleClassBtn = document.getElementById('toggleClassBtn'); const removeClassBtn = document.getElementById('removeClassBtn'); addClassBtn.addEventListener('click', function() { // 添加类名 classElement.classList.add('highlight'); }); toggleClassBtn.addEventListener('click', function() { // 切换类名(如果存在则移除,不存在则添加) classElement.classList.toggle('rounded'); }); removeClassBtn.addEventListener('click', function() { // 移除类名 classElement.classList.remove('highlight'); }); </script>
创建和删除元素
创建新元素
使用document.createElement
方法创建新元素,然后使用appendChild
或insertBefore
将其添加到DOM中。
<div id="container"> <p>原始段落</p> </div> <button id="addElementBtn">添加元素</button> <script> const container = document.getElementById('container'); const addElementBtn = document.getElementById('addElementBtn'); addElementBtn.addEventListener('click', function() { // 创建新元素 const newParagraph = document.createElement('p'); // 设置元素内容 newParagraph.textContent = '这是新添加的段落'; // 添加样式 newParagraph.style.color = 'red'; newParagraph.style.fontWeight = 'bold'; // 将新元素添加到容器中 container.appendChild(newParagraph); }); </script>
删除元素
使用removeChild
方法或remove
方法删除元素。
<div id="elementsContainer"> <div class="item">项目1 <button class="deleteBtn">删除</button></div> <div class="item">项目2 <button class="deleteBtn">删除</button></div> <div class="item">项目3 <button class="deleteBtn">删除</button></div> </div> <script> const elementsContainer = document.getElementById('elementsContainer'); // 使用事件委托处理删除按钮的点击事件 elementsContainer.addEventListener('click', function(event) { // 检查点击的是否是删除按钮 if (event.target.classList.contains('deleteBtn')) { // 获取要删除的项目 const itemToDelete = event.target.parentElement; // 从父元素中删除子元素 elementsContainer.removeChild(itemToDelete); // 或者直接使用remove方法 // itemToDelete.remove(); } }); </script>
克隆元素
使用cloneNode
方法克隆元素,参数true
表示深度克隆(包括所有子元素)。
<div id="original"> <h2>原始元素</h2> <p>这是一个段落。</p> </div> <button id="cloneBtn">克隆元素</button> <div id="clonesContainer"></div> <script> const original = document.getElementById('original'); const cloneBtn = document.getElementById('cloneBtn'); const clonesContainer = document.getElementById('clonesContainer'); cloneBtn.addEventListener('click', function() { // 深度克隆原始元素 const clonedElement = original.cloneNode(true); // 修改克隆元素的ID以避免重复 clonedElement.id = 'cloned-' + Date.now(); // 将克隆元素添加到容器中 clonesContainer.appendChild(clonedElement); }); </script>
事件处理
事件监听器
使用addEventListener
方法为元素添加事件监听器。
<button id="clickBtn">点击我</button> <div id="output"></div> <script> const clickBtn = document.getElementById('clickBtn'); const output = document.getElementById('output'); // 添加点击事件监听器 clickBtn.addEventListener('click', function() { output.textContent = '按钮被点击了!'; output.style.color = 'green'; }); // 添加鼠标悬停事件监听器 clickBtn.addEventListener('mouseover', function() { this.style.backgroundColor = 'lightblue'; }); // 添加鼠标离开事件监听器 clickBtn.addEventListener('mouseout', function() { this.style.backgroundColor = ''; }); </script>
事件对象
事件处理函数接收一个事件对象,包含关于事件的详细信息。
<div id="mouseArea" style="width: 300px; height: 200px; background-color: #f0f0f0; padding: 10px;"> 在此区域移动鼠标 </div> <div id="mouseInfo"></div> <script> const mouseArea = document.getElementById('mouseArea'); const mouseInfo = document.getElementById('mouseInfo'); mouseArea.addEventListener('mousemove', function(event) { // 获取鼠标位置 const x = event.clientX; const y = event.clientY; // 显示鼠标位置信息 mouseInfo.innerHTML = ` <p>鼠标位置: X=${x}, Y=${y}</p> <p>相对于元素: X=${event.offsetX}, Y=${event.offsetY}</p> `; }); </script>
事件冒泡和捕获
事件在DOM中的传播分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
<style> .outer { width: 300px; height: 300px; background-color: #f0f0f0; padding: 20px; } .middle { width: 200px; height: 200px; background-color: #d0d0d0; padding: 20px; } .inner { width: 100px; height: 100px; background-color: #b0b0b0; padding: 20px; } </style> <div id="outer" class="outer"> 外层元素 <div id="middle" class="middle"> 中层元素 <div id="inner" class="inner"> 内层元素 </div> </div> </div> <div id="eventLog"></div> <script> const outer = document.getElementById('outer'); const middle = document.getElementById('middle'); const inner = document.getElementById('inner'); const eventLog = document.getElementById('eventLog'); // 添加点击事件监听器(冒泡阶段) outer.addEventListener('click', function(event) { logEvent('外层元素 - 冒泡阶段'); }); middle.addEventListener('click', function(event) { logEvent('中层元素 - 冒泡阶段'); }); inner.addEventListener('click', function(event) { logEvent('内层元素 - 冒泡阶段'); }); // 添加点击事件监听器(捕获阶段) outer.addEventListener('click', function(event) { logEvent('外层元素 - 捕获阶段'); }, true); middle.addEventListener('click', function(event) { logEvent('中层元素 - 捕获阶段'); }, true); inner.addEventListener('click', function(event) { logEvent('内层元素 - 捕获阶段'); }, true); function logEvent(message) { const p = document.createElement('p'); p.textContent = message; eventLog.appendChild(p); } </script>
事件委托
利用事件冒泡机制,在父元素上处理子元素的事件。
<ul id="itemList"> <li data-id="1">项目1</li> <li data-id="2">项目2</li> <li data-id="3">项目3</li> </ul> <button id="addItemBtn">添加项目</button> <div id="selectedItem"></div> <script> const itemList = document.getElementById('itemList'); const addItemBtn = document.getElementById('addItemBtn'); const selectedItem = document.getElementById('selectedItem'); let itemCount = 3; // 使用事件委托处理列表项的点击事件 itemList.addEventListener('click', function(event) { // 检查点击的是否是列表项 if (event.target.tagName === 'LI') { // 获取项目ID const itemId = event.target.dataset.id; // 显示选中的项目 selectedItem.textContent = `选中的项目ID: ${itemId}, 内容: ${event.target.textContent}`; // 高亮选中的项目 // 移除所有项目的高亮 document.querySelectorAll('#itemList li').forEach(li => { li.style.backgroundColor = ''; }); // 高亮当前项目 event.target.style.backgroundColor = 'lightblue'; } }); // 添加新项目 addItemBtn.addEventListener('click', function() { itemCount++; const newItem = document.createElement('li'); newItem.dataset.id = itemCount; newItem.textContent = `项目${itemCount}`; itemList.appendChild(newItem); }); </script>
实战案例
动态待办事项列表
创建一个可以添加、删除和标记完成状态的待办事项列表。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>待办事项列表</title> <style> body { font-family: 'Arial', sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; } h1 { text-align: center; color: #333; } .input-container { display: flex; margin-bottom: 20px; } #todoInput { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; } #addBtn { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; margin-left: 10px; cursor: pointer; font-size: 16px; } #addBtn:hover { background-color: #45a049; } #todoList { list-style: none; padding: 0; } .todo-item { display: flex; align-items: center; padding: 12px; border-bottom: 1px solid #eee; } .todo-item:last-child { border-bottom: none; } .todo-text { flex: 1; margin-left: 10px; } .completed { text-decoration: line-through; color: #888; } .delete-btn { background-color: #f44336; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; } .delete-btn:hover { background-color: #d32f2f; } .empty-state { text-align: center; color: #888; padding: 20px; } </style> </head> <body> <h1>待办事项列表</h1> <div class="input-container"> <input type="text" id="todoInput" placeholder="添加新的待办事项..."> <button id="addBtn">添加</button> </div> <ul id="todoList"> <li class="empty-state">暂无待办事项,请添加新的项目</li> </ul> <script> // 获取DOM元素 const todoInput = document.getElementById('todoInput'); const addBtn = document.getElementById('addBtn'); const todoList = document.getElementById('todoList'); // 初始化待办事项数组 let todos = []; // 添加待办事项 function addTodo() { const todoText = todoInput.value.trim(); if (todoText === '') { alert('请输入待办事项内容'); return; } // 创建新的待办事项对象 const newTodo = { id: Date.now(), text: todoText, completed: false }; // 添加到数组 todos.push(newTodo); // 清空输入框 todoInput.value = ''; // 渲染列表 renderTodos(); } // 删除待办事项 function deleteTodo(id) { // 从数组中过滤掉要删除的项目 todos = todos.filter(todo => todo.id !== id); // 渲染列表 renderTodos(); } // 切换待办事项完成状态 function toggleTodo(id) { // 查找并更新待办事项 todos = todos.map(todo => { if (todo.id === id) { return { ...todo, completed: !todo.completed }; } return todo; }); // 渲染列表 renderTodos(); } // 渲染待办事项列表 function renderTodos() { // 清空列表 todoList.innerHTML = ''; // 检查是否有待办事项 if (todos.length === 0) { const emptyState = document.createElement('li'); emptyState.className = 'empty-state'; emptyState.textContent = '暂无待办事项,请添加新的项目'; todoList.appendChild(emptyState); return; } // 为每个待办事项创建列表项 todos.forEach(todo => { const li = document.createElement('li'); li.className = 'todo-item'; // 创建复选框 const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = todo.completed; checkbox.addEventListener('change', () => toggleTodo(todo.id)); // 创建文本元素 const text = document.createElement('span'); text.className = 'todo-text'; text.textContent = todo.text; if (todo.completed) { text.classList.add('completed'); } // 创建删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-btn'; deleteBtn.textContent = '删除'; deleteBtn.addEventListener('click', () => deleteTodo(todo.id)); // 添加元素到列表项 li.appendChild(checkbox); li.appendChild(text); li.appendChild(deleteBtn); // 添加列表项到列表 todoList.appendChild(li); }); } // 添加事件监听器 addBtn.addEventListener('click', addTodo); // 按Enter键添加待办事项 todoInput.addEventListener('keypress', function(event) { if (event.key === 'Enter') { addTodo(); } }); // 初始渲染 renderTodos(); </script> </body> </html>
动态表单生成器
创建一个可以动态添加和删除表单字段的表单生成器。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>动态表单生成器</title> <style> body { font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } h1 { text-align: center; color: #333; } .form-builder { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background-color: #f9f9f9; } .field-controls { margin-bottom: 15px; } .field-controls select, .field-controls input { padding: 8px; margin-right: 10px; border: 1px solid #ddd; border-radius: 4px; } .add-field-btn { padding: 8px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; } .add-field-btn:hover { background-color: #45a049; } .form-preview { padding: 20px; border: 1px solid #ddd; border-radius: 5px; background-color: #fff; } .form-field { margin-bottom: 15px; } .form-field label { display: block; margin-bottom: 5px; font-weight: bold; } .form-field input, .form-field textarea, .form-field select { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; } .form-field textarea { resize: vertical; min-height: 100px; } .field-actions { margin-top: 5px; } .remove-field-btn { padding: 5px 10px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; } .remove-field-btn:hover { background-color: #d32f2f; } .form-actions { margin-top: 20px; text-align: right; } .submit-btn { padding: 10px 20px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } .submit-btn:hover { background-color: #0b7dda; } .form-data { margin-top: 30px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; background-color: #f9f9f9; display: none; } .form-data h3 { margin-top: 0; } .form-data pre { background-color: #eee; padding: 10px; border-radius: 4px; white-space: pre-wrap; } </style> </head> <body> <h1>动态表单生成器</h1> <div class="form-builder"> <h2>表单构建器</h2> <div class="field-controls"> <select id="fieldType"> <option value="text">文本输入</option> <option value="email">邮箱</option> <option value="password">密码</option> <option value="number">数字</option> <option value="textarea">文本区域</option> <option value="select">下拉选择</option> <option value="checkbox">复选框</option> <option value="radio">单选按钮</option> </select> <input type="text" id="fieldLabel" placeholder="字段标签"> <input type="text" id="fieldName" placeholder="字段名称"> <button id="addFieldBtn" class="add-field-btn">添加字段</button> </div> </div> <div class="form-preview"> <h2>表单预览</h2> <form id="dynamicForm"> <!-- 动态生成的表单字段将在这里显示 --> </form> <div class="form-actions"> <button type="button" id="submitBtn" class="submit-btn">提交表单</button> </div> </div> <div id="formData" class="form-data"> <h3>表单数据</h3> <pre id="formDataOutput"></pre> </div> <script> // 获取DOM元素 const fieldType = document.getElementById('fieldType'); const fieldLabel = document.getElementById('fieldLabel'); const fieldName = document.getElementById('fieldName'); const addFieldBtn = document.getElementById('addFieldBtn'); const dynamicForm = document.getElementById('dynamicForm'); const submitBtn = document.getElementById('submitBtn'); const formData = document.getElementById('formData'); const formDataOutput = document.getElementById('formDataOutput'); // 存储表单字段 let formFields = []; // 添加表单字段 function addFormField() { const type = fieldType.value; const label = fieldLabel.value.trim(); const name = fieldName.value.trim(); if (label === '' || name === '') { alert('请填写字段标签和名称'); return; } // 创建字段对象 const field = { id: Date.now(), type: type, label: label, name: name, options: type === 'select' || type === 'radio' ? getOptions() : [] }; // 添加到字段数组 formFields.push(field); // 清空输入 fieldLabel.value = ''; fieldName.value = ''; // 渲染表单 renderForm(); } // 获取选项(用于下拉选择、单选按钮等) function getOptions() { const options = []; const optionCount = prompt('请输入选项数量:', '3'); if (optionCount && !isNaN(optionCount)) { const count = parseInt(optionCount); for (let i = 0; i < count; i++) { const optionValue = prompt(`请输入选项 ${i + 1} 的值:`, `选项 ${i + 1}`); if (optionValue) { options.push(optionValue); } } } return options; } // 删除表单字段 function removeFormField(id) { formFields = formFields.filter(field => field.id !== id); renderForm(); } // 渲染表单 function renderForm() { // 清空表单 dynamicForm.innerHTML = ''; // 检查是否有字段 if (formFields.length === 0) { const emptyMessage = document.createElement('p'); emptyMessage.textContent = '暂无表单字段,请添加字段'; emptyMessage.style.textAlign = 'center'; emptyMessage.style.color = '#888'; dynamicForm.appendChild(emptyMessage); return; } // 为每个字段创建表单元素 formFields.forEach(field => { const fieldDiv = document.createElement('div'); fieldDiv.className = 'form-field'; // 创建标签 const label = document.createElement('label'); label.textContent = field.label; fieldDiv.appendChild(label); // 根据字段类型创建不同的输入元素 let inputElement; switch (field.type) { case 'textarea': inputElement = document.createElement('textarea'); inputElement.name = field.name; break; case 'select': inputElement = document.createElement('select'); inputElement.name = field.name; // 添加默认选项 const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '请选择...'; inputElement.appendChild(defaultOption); // 添加用户定义的选项 field.options.forEach(option => { const optionElement = document.createElement('option'); optionElement.value = option; optionElement.textContent = option; inputElement.appendChild(optionElement); }); break; case 'checkbox': inputElement = document.createElement('input'); inputElement.type = 'checkbox'; inputElement.name = field.name; inputElement.value = 'yes'; break; case 'radio': // 为单选按钮创建一个容器 const radioContainer = document.createElement('div'); field.options.forEach((option, index) => { const radioDiv = document.createElement('div'); radioDiv.style.marginBottom = '5px'; const radio = document.createElement('input'); radio.type = 'radio'; radio.name = field.name; radio.value = option; radio.id = `${field.name}_${index}`; const radioLabel = document.createElement('label'); radioLabel.textContent = option; radioLabel.htmlFor = `${field.name}_${index}`; radioLabel.style.marginLeft = '5px'; radioLabel.style.fontWeight = 'normal'; radioDiv.appendChild(radio); radioDiv.appendChild(radioLabel); radioContainer.appendChild(radioDiv); }); fieldDiv.appendChild(radioContainer); break; default: inputElement = document.createElement('input'); inputElement.type = field.type; inputElement.name = field.name; } // 如果不是单选按钮,添加输入元素 if (field.type !== 'radio') { fieldDiv.appendChild(inputElement); } // 添加删除按钮 const actionsDiv = document.createElement('div'); actionsDiv.className = 'field-actions'; const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'remove-field-btn'; removeBtn.textContent = '删除字段'; removeBtn.addEventListener('click', () => removeFormField(field.id)); actionsDiv.appendChild(removeBtn); fieldDiv.appendChild(actionsDiv); // 添加字段到表单 dynamicForm.appendChild(fieldDiv); }); } // 提交表单 function submitForm() { if (formFields.length === 0) { alert('表单没有字段,无法提交'); return; } // 创建FormData对象 const formData = new FormData(dynamicForm); // 转换为普通对象 const formDataObj = {}; // 处理常规字段 for (let [key, value] of formData.entries()) { // 如果字段已存在,转换为数组 if (formDataObj[key]) { if (!Array.isArray(formDataObj[key])) { formDataObj[key] = [formDataObj[key]]; } formDataObj[key].push(value); } else { formDataObj[key] = value; } } // 处理复选框(如果未选中,不会包含在FormData中) formFields.forEach(field => { if (field.type === 'checkbox' && !formDataObj[field.name]) { formDataObj[field.name] = 'no'; } }); // 显示表单数据 formData.style.display = 'block'; formDataOutput.textContent = JSON.stringify(formDataObj, null, 2); } // 添加事件监听器 addFieldBtn.addEventListener('click', addFormField); submitBtn.addEventListener('click', submitForm); // 初始渲染 renderForm(); </script> </body> </html>
实时搜索过滤器
创建一个实时搜索过滤器,可以根据用户输入过滤列表项。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>实时搜索过滤器</title> <style> body { font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } h1 { text-align: center; color: #333; } .search-container { margin-bottom: 20px; } #searchInput { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; box-sizing: border-box; } .filter-options { margin: 15px 0; display: flex; flex-wrap: wrap; gap: 10px; } .filter-option { display: flex; align-items: center; } .filter-option input { margin-right: 5px; } .item-list { list-style: none; padding: 0; } .item { padding: 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; } .item:last-child { border-bottom: none; } .item-icon { width: 50px; height: 50px; border-radius: 50%; background-color: #4CAF50; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 15px; flex-shrink: 0; } .item-content { flex: 1; } .item-title { font-weight: bold; margin-bottom: 5px; } .item-description { color: #666; font-size: 14px; } .item-category { display: inline-block; padding: 3px 8px; background-color: #e0e0e0; border-radius: 12px; font-size: 12px; margin-top: 5px; } .no-results { text-align: center; padding: 30px; color: #888; font-style: italic; } .highlight { background-color: yellow; font-weight: bold; } .stats { margin-top: 20px; padding: 10px; background-color: #f5f5f5; border-radius: 4px; text-align: center; } </style> </head> <body> <h1>实时搜索过滤器</h1> <div class="search-container"> <input type="text" id="searchInput" placeholder="搜索项目..."> </div> <div class="filter-options"> <div class="filter-option"> <input type="checkbox" id="filterFruits" value="水果" checked> <label for="filterFruits">水果</label> </div> <div class="filter-option"> <input type="checkbox" id="filterVegetables" value="蔬菜" checked> <label for="filterVegetables">蔬菜</label> </div> <div class="filter-option"> <input type="checkbox" id="filterGrains" value="谷物" checked> <label for="filterGrains">谷物</label> </div> <div class="filter-option"> <input type="checkbox" id="filterProteins" value="蛋白质" checked> <label for="filterProteins">蛋白质</label> </div> </div> <ul id="itemList" class="item-list"> <!-- 项目将通过JavaScript动态生成 --> </ul> <div id="stats" class="stats"> 显示 0 / 0 个项目 </div> <script> // 示例数据 const items = [ { id: 1, title: '苹果', description: '富含维生素C和纤维的水果', category: '水果' }, { id: 2, title: '香蕉', description: '富含钾的热带水果', category: '水果' }, { id: 3, title: '橙子', description: '富含维生素C的柑橘类水果', category: '水果' }, { id: 4, title: '草莓', description: '富含抗氧化剂的浆果', category: '水果' }, { id: 5, title: '胡萝卜', description: '富含维生素A的根茎蔬菜', category: '蔬菜' }, { id: 6, title: '西兰花', description: '富含维生素K和C的绿色蔬菜', category: '蔬菜' }, { id: 7, title: '菠菜', description: '富含铁和叶酸的绿叶蔬菜', category: '蔬菜' }, { id: 8, title: '番茄', description: '富含番茄红素的红色蔬果', category: '蔬菜' }, { id: 9, title: '大米', description: '主要的主食谷物', category: '谷物' }, { id: 10, title: '小麦', description: '制作面包和面食的谷物', category: '谷物' }, { id: 11, title: '燕麦', description: '富含可溶性纤维的谷物', category: '谷物' }, { id: 12, title: '玉米', description: '多用途的谷物', category: '谷物' }, { id: 13, title: '鸡肉', description: '优质蛋白质来源', category: '蛋白质' }, { id: 14, title: '鱼类', description: '富含Omega-3脂肪酸的蛋白质', category: '蛋白质' }, { id: 15, title: '豆腐', description: '植物性蛋白质来源', category: '蛋白质' }, { id: 16, title: '鸡蛋', description: '完整蛋白质来源', category: '蛋白质' } ]; // 获取DOM元素 const searchInput = document.getElementById('searchInput'); const filterOptions = document.querySelectorAll('.filter-option input'); const itemList = document.getElementById('itemList'); const stats = document.getElementById('stats'); // 获取选中的类别 function getSelectedCategories() { const selectedCategories = []; filterOptions.forEach(option => { if (option.checked) { selectedCategories.push(option.value); } }); return selectedCategories; } // 高亮匹配的文本 function highlightText(text, searchTerm) { if (!searchTerm) return text; const regex = new RegExp(`(${searchTerm})`, 'gi'); return text.replace(regex, '<span class="highlight">$1</span>'); } // 渲染项目列表 function renderItems() { const searchTerm = searchInput.value.trim().toLowerCase(); const selectedCategories = getSelectedCategories(); // 过滤项目 const filteredItems = items.filter(item => { // 检查类别是否被选中 if (!selectedCategories.includes(item.category)) { return false; } // 检查是否匹配搜索词 if (searchTerm) { const titleMatch = item.title.toLowerCase().includes(searchTerm); const descriptionMatch = item.description.toLowerCase().includes(searchTerm); return titleMatch || descriptionMatch; } return true; }); // 清空列表 itemList.innerHTML = ''; // 检查是否有结果 if (filteredItems.length === 0) { const noResults = document.createElement('li'); noResults.className = 'no-results'; noResults.textContent = '没有找到匹配的项目'; itemList.appendChild(noResults); } else { // 渲染每个项目 filteredItems.forEach(item => { const li = document.createElement('li'); li.className = 'item'; // 创建图标(使用首字母) const icon = document.createElement('div'); icon.className = 'item-icon'; icon.textContent = item.title.charAt(0); // 根据类别设置不同的背景色 switch (item.category) { case '水果': icon.style.backgroundColor = '#FF9800'; break; case '蔬菜': icon.style.backgroundColor = '#4CAF50'; break; case '谷物': icon.style.backgroundColor = '#FFC107'; break; case '蛋白质': icon.style.backgroundColor = '#F44336'; break; } // 创建内容区域 const content = document.createElement('div'); content.className = 'item-content'; // 创建标题(高亮匹配的文本) const title = document.createElement('div'); title.className = 'item-title'; title.innerHTML = highlightText(item.title, searchTerm); // 创建描述(高亮匹配的文本) const description = document.createElement('div'); description.className = 'item-description'; description.innerHTML = highlightText(item.description, searchTerm); // 创建类别标签 const category = document.createElement('div'); category.className = 'item-category'; category.textContent = item.category; // 组装内容 content.appendChild(title); content.appendChild(description); content.appendChild(category); // 组装项目 li.appendChild(icon); li.appendChild(content); // 添加到列表 itemList.appendChild(li); }); } // 更新统计信息 updateStats(filteredItems.length, items.length); } // 更新统计信息 function updateStats(visibleCount, totalCount) { stats.textContent = `显示 ${visibleCount} / ${totalCount} 个项目`; } // 添加事件监听器 searchInput.addEventListener('input', renderItems); filterOptions.forEach(option => { option.addEventListener('change', renderItems); }); // 初始渲染 renderItems(); </script> </body> </html>
最佳实践和性能优化
避免频繁的DOM操作
频繁的DOM操作会导致性能问题,因为每次操作都可能引起浏览器的重排或重绘。
<div id="container"></div> <button id="addItemsBtn">添加1000个项目</button> <script> const container = document.getElementById('container'); const addItemsBtn = document.getElementById('addItemsBtn'); addItemsBtn.addEventListener('click', function() { // 不好的做法:频繁操作DOM // for (let i = 0; i < 1000; i++) { // const item = document.createElement('div'); // item.textContent = `项目 ${i + 1}`; // container.appendChild(item); // } // 好的做法:使用文档片段减少DOM操作 const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const item = document.createElement('div'); item.textContent = `项目 ${i + 1}`; fragment.appendChild(item); } container.appendChild(fragment); }); </script>
使用事件委托
当有大量元素需要相同的事件处理时,使用事件委托可以减少内存使用和提高性能。
<ul id="list"> <!-- 动态生成的列表项 --> </ul> <button id="addItemBtn">添加项目</button> <script> const list = document.getElementById('list'); const addItemBtn = document.getElementById('addItemBtn'); let itemCount = 0; // 添加项目 addItemBtn.addEventListener('click', function() { itemCount++; const item = document.createElement('li'); item.textContent = `项目 ${itemCount}`; item.className = 'list-item'; list.appendChild(item); }); // 不好的做法:为每个列表项单独添加事件监听器 // function addItemClickListeners() { // const items = document.querySelectorAll('.list-item'); // items.forEach(item => { // item.addEventListener('click', function() { // console.log(`点击了: ${this.textContent}`); // }); // }); // } // 好的做法:使用事件委托 list.addEventListener('click', function(event) { // 检查点击的是否是列表项 if (event.target.tagName === 'LI') { console.log(`点击了: ${event.target.textContent}`); } }); </script>
避免强制同步布局
强制同步布局(Layout Thrashing)是指JavaScript强制浏览器在执行过程中重新计算布局,这会导致性能问题。
<div id="box" style="width: 100px; height: 100px; background-color: blue;"></div> <button id="animateBtn">动画</button> <script> const box = document.getElementById('box'); const animateBtn = document.getElementById('animateBtn'); animateBtn.addEventListener('click', function() { // 不好的做法:强制同步布局 // for (let i = 0; i < 100; i++) { // box.style.width = (100 + i) + 'px'; // // 读取宽度会导致强制同步布局 // console.log(box.offsetWidth); // } // 好的做法:批量读取和写入 let width = 100; // 先读取所有需要的值 const startWidth = box.offsetWidth; // 然后批量写入 for (let i = 0; i < 100; i++) { width++; box.style.width = width + 'px'; } // 最后读取最终值 console.log('最终宽度:', box.offsetWidth); }); </script>
使用requestAnimationFrame进行动画
使用requestAnimationFrame
而不是setTimeout
或setInterval
进行动画,可以获得更好的性能和更流畅的动画效果。
<div id="animatedBox" style="width: 50px; height: 50px; background-color: red; position: absolute; top: 0; left: 0;"></div> <button id="startAnimationBtn">开始动画</button> <script> const animatedBox = document.getElementById('animatedBox'); const startAnimationBtn = document.getElementById('startAnimationBtn'); let animationId = null; let position = 0; startAnimationBtn.addEventListener('click', function() { if (animationId) { // 如果动画已经在运行,先停止 cancelAnimationFrame(animationId); animationId = null; startAnimationBtn.textContent = '开始动画'; } else { // 开始动画 startAnimationBtn.textContent = '停止动画'; animate(); } }); function animate() { // 更新位置 position += 2; // 如果超出边界,重置位置 if (position > window.innerWidth - 50) { position = 0; } // 更新元素位置 animatedBox.style.left = position + 'px'; // 请求下一帧 animationId = requestAnimationFrame(animate); } </script>
使用虚拟滚动处理大量数据
当需要渲染大量数据时,使用虚拟滚动技术只渲染可见区域的项目,可以大大提高性能。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>虚拟滚动示例</title> <style> body { font-family: 'Arial', sans-serif; margin: 0; padding: 20px; } h1 { text-align: center; } .scroll-container { height: 400px; overflow-y: auto; border: 1px solid #ddd; position: relative; } .scroll-content { position: relative; } .scroll-item { padding: 15px; border-bottom: 1px solid #eee; box-sizing: border-box; height: 50px; } .scroll-item:nth-child(odd) { background-color: #f9f9f9; } .info { margin-top: 10px; text-align: center; color: #666; } </style> </head> <body> <h1>虚拟滚动示例</h1> <div id="scrollContainer" class="scroll-container"> <div id="scrollContent" class="scroll-content"> <!-- 虚拟滚动项将在这里动态生成 --> </div> </div> <div id="info" class="info"> 显示 0 / 0 个项目 </div> <script> // 配置 const config = { itemHeight: 50, // 每个项目的高度 bufferSize: 5, // 上下缓冲的项目数量 totalItems: 10000 // 总项目数 }; // 获取DOM元素 const scrollContainer = document.getElementById('scrollContainer'); const scrollContent = document.getElementById('scrollContent'); const info = document.getElementById('info'); // 状态 let scrollTop = 0; let visibleStartIndex = 0; let visibleEndIndex = 0; // 初始化 function init() { // 设置滚动内容的高度 scrollContent.style.height = `${config.itemHeight * config.totalItems}px`; // 监听滚动事件 scrollContainer.addEventListener('scroll', handleScroll); // 初始渲染 renderVisibleItems(); } // 处理滚动事件 function handleScroll() { scrollTop = scrollContainer.scrollTop; // 计算可见区域的起始和结束索引 visibleStartIndex = Math.floor(scrollTop / config.itemHeight); visibleEndIndex = Math.min( visibleStartIndex + Math.ceil(scrollContainer.clientHeight / config.itemHeight), config.totalItems - 1 ); // 添加缓冲区 const bufferedStartIndex = Math.max(0, visibleStartIndex - config.bufferSize); const bufferedEndIndex = Math.min(config.totalItems - 1, visibleEndIndex + config.bufferSize); // 渲染可见项目 renderItems(bufferedStartIndex, bufferedEndIndex); // 更新信息 updateInfo(); } // 渲染指定范围的项目 function renderItems(startIndex, endIndex) { // 清空当前内容 scrollContent.innerHTML = ''; // 设置内容的位置 scrollContent.style.transform = `translateY(${startIndex * config.itemHeight}px)`; // 渲染项目 for (let i = startIndex; i <= endIndex; i++) { const item = createItem(i); scrollContent.appendChild(item); } } // 创建单个项目 function createItem(index) { const item = document.createElement('div'); item.className = 'scroll-item'; item.textContent = `项目 ${index + 1}`; return item; } // 渲染可见项目(初始调用) function renderVisibleItems() { visibleEndIndex = Math.min( Math.ceil(scrollContainer.clientHeight / config.itemHeight), config.totalItems - 1 ); renderItems(visibleStartIndex, visibleEndIndex); updateInfo(); } // 更新信息 function updateInfo() { const visibleCount = visibleEndIndex - visibleStartIndex + 1; info.textContent = `显示 ${visibleCount} / ${config.totalItems} 个项目 (索引: ${visibleStartIndex}-${visibleEndIndex})`; } // 初始化虚拟滚动 init(); </script> </body> </html>
结论
JavaScript操作DOM是动态网页开发的核心技能。通过本教程,我们学习了如何选择DOM元素、修改元素内容和属性、处理用户事件以及创建动态交互效果。通过实际案例,我们掌握了待办事项列表、动态表单生成器和实时搜索过滤器的实现方法。同时,我们也了解了DOM操作的最佳实践和性能优化技巧,如避免频繁DOM操作、使用事件委托、避免强制同步布局、使用requestAnimationFrame进行动画以及虚拟滚动处理大量数据。
掌握这些技能后,你将能够创建更加动态、交互式和高性能的Web应用。记住,实践是掌握这些技能的关键,所以请尝试将这些技术应用到你的项目中,不断探索和学习新的DOM操作技巧。