JavaScript DOM操作源代码:元素增删改查与事件绑定实例
深入解析原生 JavaScript DOM 操作核心技术,通过可运行的源代码实例,系统剖析元素增、删、改、查 API 原理、性能优化方法及现代事件委托绑定实践。
一、元素查询(查):精准定位 DOM 节点
在对页面元素进行任何操作之前,首先需要获取目标节点的内存引用。现代 Web 规范主要提供了两类查找方式:基于专用属性的经典查询与基于 CSS 选择器的复合查询。
1. 核心查询 API 与适用场景
在实际工程中,document.getElementById 拥有专属的哈希索引表,查询速度最为迅速,适用于页面中唯一的顶级容器或关键控件;而 document.querySelector 与 document.querySelectorAll 能够使用包括属性选择器、伪类在内的任意 CSS 选择规则,灵活性极高。
// 1. 通过 ID 极速获取单个节点
const appContainer = document.getElementById('app');
// 2. 通过 CSS 选择器获取首个匹配的子节点
const activeItem = document.querySelector('.todo-list .todo-item.active');
// 3. 获取所有匹配节点的静态集合(NodeList)
const allItems = document.querySelectorAll('.todo-list .todo-item');
// 遍历 NodeList(现代浏览器原生支持 forEach)
allItems.forEach((item, index) => {
console.log(`第 ${index + 1} 项内容:`, item.textContent.trim());
});
2. 关键机制差异:静态 NodeList 与动态 HTMLCollection
理解两者的差异至关重要:通过 querySelectorAll 返回的是静态快照(Static NodeList),随后无论 DOM 结构如何增减,该集合不会自动变化;而通过 getElementsByClassName 或 getElementsByTagName 返回的是动态集合(Live HTMLCollection),DOM 的任何变更会实时映射回集合中。在循环遍历时,修改 DOM 如果依赖动态集合,极易因下标偏移产生无限循环或漏项错误,因此工程中更推荐使用静态快照 querySelectorAll。
二、元素创建与插入(增):构建与挂载新节点
向页面中添加新元素分为两个独立阶段:首先在内存中生成游离的节点并完成内容装配,然后通过挂载方法将其正式插入到文档渲染树中。
1. 元素创建与层级挂载步骤
使用 document.createElement(tagName) 创建新元素。该节点在挂载前完全存在于内存中,对其进行属性赋值或样式配置不会触发浏览器的重排(Reflow)与重绘(Repaint)。
2. 单个节点挂载完整源代码
// 目标父容器
const listContainer = document.querySelector('#todoList');
// 步骤 1:在内存中创建元素节点
const newLi = document.createElement('li');
// 步骤 2:装配属性与样式类
newLi.className = 'todo-item priority-high';
newLi.dataset.id = 'task-2048';
// 步骤 3:注入安全文本与子元素
const textSpan = document.createElement('span');
textSpan.className = 'item-text';
textSpan.textContent = '完成财务报表审计工作';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-delete';
deleteBtn.textContent = '删除';
newLi.append(textSpan, deleteBtn);
// 步骤 4:挂载到真实 DOM 中(插入到末尾)
listContainer.append(newLi);
// 若需置顶插入,可使用 prepend
// listContainer.prepend(newLi);
parent.appendChild(),否则每一次调用都会触发生效渲染。应使用 document.createDocumentFragment() 创建轻量级的虚拟文档片段,将所有子节点先装配到片段中,最后单次追加到父级容器中,把数百次渲染合并为单次开销。
// 批量高性能插入范例
function renderBatchItems(container, dataList) {
const fragment = document.createDocumentFragment();
dataList.forEach(data => {
const li = document.createElement('li');
li.className = 'data-row';
li.textContent = data.title;
fragment.appendChild(li); // 仅在内存片段中暂存,不触发浏览器重排
});
// 一次性挂载整个片段,片段本身不会作为节点进入 DOM
container.appendChild(fragment);
}
三、元素内容与属性修改(改):状态与表现同步
修改 DOM 主要包含两个维度:一是节点内部文本与 HTML 内容的变更;二是节点属性、类名与行内样式的动态切换。
1. 内容更新:textContent 与 innerHTML 的关键差异
修改元素内容最常用的属性是 textContent 与 innerHTML。它们不仅在语法行为上不同,更关系到整站的安全基线。
| 对比维度 | textContent(强烈推荐) | innerHTML(审慎使用) |
|---|---|---|
| 数据解析机制 | 纯文本解析,不解析任何 HTML 标签 | 调用 HTML 词法解析器,构建完整子 DOM 树 |
| XSS 安全防范 | 绝对安全,自动转义特殊字符(如 <script>) | 极具风险,拼接未经校验的用户输入将导致脚本执行 |
| 既有事件监听 | 仅变更文本节点,不影响容器自身监听 | 彻底销毁并重建所有子节点,原有子元素事件全部丢失 |
| 执行性能开销 | 轻量快捷,无解析损耗 | 消耗额外 CPU 资源进行语法树构建与解析 |
2. 属性与类名修改源代码
const banner = document.querySelector('#promoBanner');
// 1. 类名增删切换(推荐使用 classList API)
banner.classList.add('visible'); // 添加类
banner.classList.remove('hidden'); // 移除类
banner.classList.toggle('expanded'); // 切换状态类(有则删,无则加)
const isExpanded = banner.classList.contains('expanded'); // 状态检测
// 2. HTML 标准属性操作
banner.setAttribute('role', 'alert');
banner.setAttribute('aria-hidden', 'false');
const currentRole = banner.getAttribute('role');
// 3. 自定义数据集操作(dataset 自动映射 data-* 属性)
banner.dataset.campaignId = "2026-spring-01"; // 对应 data-campaign-id
console.log(banner.dataset.campaignId);
四、元素移除(删):安全清理与内存管理
删除 DOM 节点不仅要将元素从视觉树中剥离,还需要确保 JavaScript 运行时的垃圾回收机制(GC)能够正常回收该节点所占用的内存。
1. 常用移除方法:el.remove() 与 parent.removeChild()
在现代浏览器中,元素自身提供了 element.remove() 方法,无需显式寻找其父级节点即可直接自我销毁;如果需要支持极为陈旧的环境,可使用传统的 parent.removeChild(child)。
// 现代直接移除(自删除)
const targetCard = document.querySelector('.card[data-id="101"]');
if (targetCard) {
targetCard.remove();
}
// 传统父节点代理移除
const parentBox = document.querySelector('#cardList');
const oldChild = parentBox.querySelector('.card.obsolete');
if (oldChild && oldChild.parentNode === parentBox) {
parentBox.removeChild(oldChild);
}
// 极速清空容器内所有子节点(现代原生 API)
parentBox.replaceChildren(); // 相比 parentBox.innerHTML = '' 性能更高且语义明确
element.remove() 将元素从 DOM 树中移除后,如果全局变量、对象属性或闭包中依然保留着对该 DOM 对象的引用,该节点将无法被垃圾回收器释放,形成“游离 DOM 泄漏”。在销毁节点后,务必将持有该节点引用的变量赋值为 null。
五、事件绑定与事件委托(事件处理):让页面产生交互
页面动态交互的核心在于事件监听。传统的做法是为每个目标节点单独绑定事件函数,但在复杂动态列表中,这种做法会导致庞大的内存开销和频繁的重新绑定工作。通过理解浏览器的事件流机制,我们能够以优雅的事件委托(Event Delegation)彻底解决这一问题。
1. 浏览器事件流的三阶段模型
当用户在屏幕上触发点击时,事件并非直接出现在目标元素上,而是经历三个明确阶段:
- 捕获阶段(Capture Phase):事件由顶层窗口(window)、文档逐步向下传递,直到目标的直接父节点。
- 目标阶段(Target Phase):事件抵达用户实际点击的最深层元素。
- 冒泡阶段(Bubble Phase):事件沿 DOM 树从目标节点原路向上逐层冒泡传递,直至 window。
2. 事件委托的核心思想与对比优势
利用事件冒泡机制,我们无需在每一个子元素上单独挂载监听器,而是在它们共同的父级容器上设置单个 addEventListener。当内部子元素被点击时,事件会冒泡到父级,父级通过检测 e.target 识别触发来源并执行对应业务。
// 获取列表父级容器
const todoList = document.querySelector('#todoList');
// 仅在父级绑定单次点击监听(事件委托)
todoList.addEventListener('click', (event) => {
// 1. 判断是否点击了删除按钮(使用 closest 兼容按钮内部图标点击场景)
const deleteBtn = event.target.closest('.btn-delete');
if (deleteBtn && todoList.contains(deleteBtn)) {
const li = deleteBtn.closest('.todo-item');
const taskId = li.dataset.id;
console.log(`执行删除任务 ID: ${taskId}`);
li.remove(); // 移除当前行
return;
}
// 2. 判断是否点击了任务完成勾选框
const checkbox = event.target.closest('.todo-checkbox');
if (checkbox && todoList.contains(checkbox)) {
const li = checkbox.closest('.todo-item');
li.classList.toggle('completed', checkbox.checked);
console.log(`任务状态切换: ${checkbox.checked ? '已完成' : '未完成'}`);
}
});
六、综合实战案例:待办清单交互原型
下面是一个将元素查找、动态创建、属性修改、节点删除与事件委托无缝结合的完整可运行范例。读者可以直接将其保存为单个 .html 文件在任意现代浏览器中运行验证。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DOM 操作综合实例</title>
<style>
.todo-app { max-width: 420px; margin: 30px auto; font-family: sans-serif; }
.todo-list { list-style: none; padding: 0; margin-top: 15px; }
.todo-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #eee; }
.todo-item.done .text { text-decoration: line-through; color: #999; }
.btn-del { background: #ff4d4f; color: #fff; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="todo-app">
<h3>待办任务管理</h3>
<div>
<input type="text" id="taskInput" placeholder="输入待办事项..." />
<button id="addBtn">添加任务</button>
</div>
<ul class="todo-list" id="taskList"></ul>
</div>
<script>
// 1. 查找关键 DOM 节点
const taskInput = document.getElementById('taskInput');
const addBtn = document.getElementById('addBtn');
const taskList = document.getElementById('taskList');
let taskIdCounter = 1;
// 2. 元素创建与新增逻辑
function handleAddTask() {
const taskText = taskInput.value.trim();
if (!taskText) return;
const li = document.createElement('li');
li.className = 'todo-item';
li.dataset.id = String(taskIdCounter++);
const span = document.createElement('span');
span.className = 'text';
span.textContent = taskText; // 安全注入纯文本
const delBtn = document.createElement('button');
delBtn.className = 'btn-del';
delBtn.textContent = '删除';
li.append(span, delBtn);
taskList.append(li);
// 清空输入框并重置焦点
taskInput.value = '';
taskInput.focus();
}
// 3. 基础事件绑定:输入框回车与添加按钮点击
addBtn.addEventListener('click', handleAddTask);
taskInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleAddTask();
});
// 4. 事件委托:在父容器处理子元素点击(完成状态修改与单项删除)
taskList.addEventListener('click', (e) => {
// 场景 A:点击删除按钮
const delBtn = e.target.closest('.btn-del');
if (delBtn) {
const li = delBtn.closest('.todo-item');
li.remove(); // 直接删除节点
return;
}
// 场景 B:点击文本切换完成状态
const textSpan = e.target.closest('.text');
if (textSpan) {
const li = textSpan.closest('.todo-item');
li.classList.toggle('done'); // 修改类名
}
});
</script>
</body>
</html>
七、核心 API 汇总与最佳实践总结
为了便于在日常开发中快速回顾与选型,下方的图谱整理了原生 DOM 操作在增、删、改、查、控五个维度的核心 API 规范与性能原则。
- 优先使用
textContent赋值:除非必须渲染已知安全的富文本标记,否则坚决杜绝用innerHTML处理用户输入。 - 批量挂载采用
DocumentFragment:将多节点的装配过程隔离在内存中,完成后一次性注入,严格抑制不必要的浏览器重排。 - 动态列表统一采用事件委托:将事件集中绑定在共同父容器上,配合
e.target.closest()进行过滤,不仅降低内存开销,更能天然支持后续动态追加的子项。 - 节点移除后切断 JS 引用:调用
element.remove()后,及时清理保存在变量或闭包中的节点缓存,保障内存被及时回收。
JS监听滚动事件:回到顶部按钮的显示与隐藏逻辑
2026-11-05 16:19:37
JavaScript怎么读写LocalStorage与SessionStorage
2026-11-05 15:40:11
anime.js 与 GSAP 对比:轻量级项目该怎么选择动画库
2026-11-05 11:24:55
flv.js 与 hls.js 在直播场景下的选型对比
2026-11-05 08:18:52
JavaScript能做什么?前端开发中的作用与应用场景解析
2026-11-04 23:17:54








