第 9 章 · DOM 操作与事件
本章目标:用 JavaScript 查找、修改、创建页面元素,并响应用户操作。贯穿案例 academy-demo(贤紫技术学院课程门户演示站)中的课程卡片、主题切换与导航交互。
前置:第 8 章 JavaScript 基础语法;建议在 academy-demo/ 目录跟练。
9.1 什么是 DOM
浏览器将 HTML 解析为文档对象模型(DOM)——一棵可编程的树,每个标签是一个节点。JavaScript 通过 DOM API 读写这棵树,实现「页面即数据」的交互。
document
└── html
├── head
└── body
├── header#site-header
├── main#course-list
│ └── article.course-card × N
└── footer
| 概念 | 说明 | academy-demo 示例 |
|---|---|---|
| 文档节点 | 整棵树的根 | document |
| 元素节点 | HTML 标签 | <article class="course-card"> |
| 文本节点 | 标签内的文字 | 课程标题字符串 |
| 属性节点 | id、class、data-* | data-course-id="fe-09" |
9.1.1 查询元素
// academy-demo/js/dom.js
const title = document.getElementById('page-title');
const firstCard = document.querySelector('.course-card');
const allCards = document.querySelectorAll('.course-card');
const navLink = document.querySelector('nav a[href="#courses"]');
| 方法 | 返回值 | 适用场景 | 注意 |
|---|---|---|---|
getElementById | 单个或 null | 唯一 id | id 勿重复 |
querySelector | 第一个匹配或 null | 复杂 CSS 选择器 | 性能:避免超深嵌套 |
querySelectorAll | 类数组 NodeList | 批量操作 | 非 Live,后续 DOM 变化不自动更新 |
getElementsByClassName | 实时 HTMLCollection | 老项目兼容 | 动态增删时会「活」更新 |
行业案例 · 贤紫优选商城:商品列表页曾用document.getElementsByTagName('div')遍历筛选,DOM 节点上万时主线程卡顿 200ms+;改为querySelectorAll('[data-sku]')+ 事件委托后,首屏交互延迟降至 30ms 内。
9.2 修改内容、属性与样式
const card = document.querySelector('.course-card');
// 文本:优先 textContent,防 XSS
card.querySelector('h3').textContent = 'DOM 操作与事件';
// HTML:仅信任来源时使用
// card.innerHTML = '<strong>新课</strong>'; // 用户输入禁止直插
// class 与无障碍属性
card.classList.add('featured');
card.classList.toggle('expanded', true);
card.setAttribute('aria-expanded', 'true');
// 行内样式(优先用 CSS 类)
card.style.setProperty('--accent', '#c2185b');
// dataset 读写 data-*
card.dataset.level = 'beginner'; // data-level
console.log(card.dataset.level);
| API | 作用 | 推荐度 |
|---|---|---|
textContent | 纯文本替换 | ★★★ 默认 |
innerHTML | 解析 HTML 片段 | ★ 仅静态模板 |
classList | 增删切换类名 | ★★★ |
setAttribute | 通用属性 | ★★★ |
element.style | 行内样式 | ★ 特例覆盖 |
9.3 创建、插入与移除节点
function createCourseCard({ id, title, desc, hours }) {
const article = document.createElement('article');
article.className = 'course-card';
article.dataset.courseId = id;
const h3 = document.createElement('h3');
h3.textContent = title;
const p = document.createElement('p');
p.className = 'desc';
p.textContent = desc;
const meta = document.createElement('span');
meta.className = 'meta';
meta.textContent = `${hours} 课时`;
article.append(h3, p, meta);
return article;
}
const grid = document.querySelector('#course-grid');
grid.appendChild(createCourseCard({
id: 'fe-09',
title: 'DOM 操作与事件',
desc: '查找、修改、监听用户操作',
hours: 4,
}));
批量渲染(模板字符串 + 转义):
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function renderCourses(courses) {
const html = courses.map(c => `
<article class="course-card" data-course-id="${escapeHtml(c.id)}">
<h3>${escapeHtml(c.title)}</h3>
<p class="desc">${escapeHtml(c.desc)}</p>
</article>
`).join('');
document.querySelector('#course-grid').innerHTML = html;
}
| 操作 | API | 说明 |
|---|---|---|
| 末尾追加 | appendChild / append | append 可一次多个 |
| 前面插入 | insertBefore / prepend | |
| 替换 | replaceChild / replaceWith | |
| 移除 | removeChild / remove | 子节点用 remove 更简洁 |
| 克隆 | cloneNode(deep) | 深拷贝含子树 |
9.4 事件机制
9.4.1 注册与移除
const themeBtn = document.querySelector('#theme-toggle');
function onThemeClick(e) {
document.body.classList.toggle('theme-dark');
localStorage.setItem('theme', document.body.classList.contains('theme-dark') ? 'dark' : 'light');
}
themeBtn.addEventListener('click', onThemeClick);
// 不再需要时:themeBtn.removeEventListener('click', onThemeClick);