Day 10: DOM 操作基础
🎯 学习目标
- 理解 DOM(文档对象模型)的概念和结构
- 掌握查找元素的方法
- 学会修改元素的内容和属性
- 理解 DOM 树的遍历方法
💡 核心概念
什么是 DOM?
DOM(Document Object Model,文档对象模型)是 HTML 和 XML 文档的编程接口。它将文档表示为节点树,允许编程语言(如 JavaScript)访问和修改文档的内容、结构和样式。
DOM 树结构示例:
<!DOCTYPE html>
<html>
<head>
<title>页面标题</title>
</head>
<body>
<div id="container">
<h1>欢迎</h1>
<p class="text">这是段落</p>
<ul>
<li>项目 1</li>
<li>项目 2</li>
</ul>
</div>
</body>
</html>
对应的 DOM 树:
Document
└── html
├── head
│ └── title
│ └── "页面标题"
└── body
└── div#container
├── h1
│ └── "欢迎"
├── p.text
│ └── "这是段落"
└── ul
├── li
│ └── "项目 1"
└── li
└── "项目 2"
节点类型
DOM 中有几种常见的节点类型:
- 元素节点(Element):HTML 标签(如
<div>,<p>) - 文本节点(Text):元素内的文本内容
- 属性节点(Attribute):元素的属性(如
class,id) - 文档节点(Document):整个文档的根节点
示例:
<p id="para1" class="text">这是一个段落</p>
- 元素节点:
<p> - 属性节点:
id="para1",class="text" - 文本节点:
"这是一个段落"
DOM 查找方式
查找元素是 DOM 操作的第一步,主要有以下方法:
- 通过 ID 查找:
getElementById() - 通过类名查找:
getElementsByClassName() - 通过标签名查找:
getElementsByTagName() - 通过选择器查找:
querySelector(),querySelectorAll()
📝 核心 API 和语法
查找元素
1. getElementById()
const element = document.getElementById('elementId');
功能:通过 ID 查找元素
返回:单个元素或 null
示例:
const title = document.getElementById('main-title');
if (title) {
console.log(title.textContent); // 输出标题文本
}
2. getElementsByClassName()
const elements = document.getElementsByClassName('classname');
功能:通过类名查找元素
返回:HTMLCollection(类数组的集合)
示例:
const items = document.getElementsByClassName('item');
for (let i = 0; i < items.length; i++) {
console.log(items[i].textContent);
}
// 使用 for...of
for (const item of items) {
console.log(item.textContent);
}
3. getElementsByTagName()
const elements = document.getElementsByTagName('tagname');
功能:通过标签名查找元素
返回:HTMLCollection
示例:
const paragraphs = document.getElementsByTagName('p');
console.log(`找到 ${paragraphs.length} 个段落`);
4. querySelector()
const element = document.querySelector('selector');
功能:使用 CSS 选择器查找第一个匹配的元素
返回:单个元素或 null
示例:
// 通过 ID
const elem1 = document.querySelector('#myId');
// 通过类
const elem2 = document.querySelector('.myClass');
// 通过标签
const elem3 = document.querySelector('div');
// 通过属性
const elem4 = document.querySelector('[data-id="123"]');
// 组合选择器
const elem5 = document.querySelector('.container p:first-child');
5. querySelectorAll()
const elements = document.querySelectorAll('selector');
功能:使用 CSS 选择器查找所有匹配的元素
返回:NodeList(节点列表)
示例:
const items = document.querySelectorAll('.item');
// NodeList 支持 forEach
items.forEach((item, index) => {
console.log(`Item ${index}: ${item.textContent}`);
});
// 转换为数组
const itemsArray = Array.from(items);
修改元素内容
1. innerText
element.innerText = '新文本';
功能:设置或获取元素的纯文本内容
特点:
- 只能设置文本,会解析为纯文本
- 获取时会触发重排(reflow)
- 不包含隐藏元素的文本
示例:
const para = document.querySelector('p');
para.innerText = '<strong>加粗文本</strong>';
// 显示为:<strong>加粗文本</strong>(不是加粗效果)
2. textContent
element.textContent = '新文本';
功能:设置或获取元素的所有文本内容(包括子元素)
特点:
- 性能更好(不触发重排)
- 包含隐藏元素的文本
示例:
const div = document.querySelector('div');
console.log(div.textContent); // 获取所有文本,包括子元素的
3. innerHTML
element.innerHTML = '<strong>HTML 内容</strong>';
功能:设置或获取元素的 HTML 内容
⚠️ 注意:存在 XSS 安全风险
示例:
const container = document.getElementById('container');
container.innerHTML = `
<h2>标题</h2>
<p>段落</p>
<ul>
<li>项目 1</li>
<li>项目 2</li>
</ul>
`;
// ⚠️ 危险:用户输入直接放入 innerHTML
const userInput = '<img src=x onerror=alert(1)>';
container.innerHTML = userInput; // XSS 攻击!
// ✅ 安全:使用 textContent 或转义
container.textContent = userInput;
修改元素属性
1. 直接属性访问
element.id = 'newId';
element.className = 'newClass';
element.href = 'https://example.com';
2. getAttribute() / setAttribute()
element.setAttribute('attribute', 'value');
const value = element.getAttribute('attribute');
示例:
const link = document.querySelector('a');
// 设置属性
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('data-id', '123');
// 获取属性
console.log(link.getAttribute('href')); // https://example.com
// 检查属性是否存在
if (link.hasAttribute('target')) {
console.log('Target 属性存在');
}
// 移除属性
link.removeAttribute('target');
3. classList 操作
element.classList.add('class1', 'class2');
element.classList.remove('class1');
element.classList.toggle('active');
element.classList.contains('active');
示例:
const button = document.querySelector('button');
// 添加类
button.classList.add('primary', 'large');
// 移除类
button.classList.remove('large');
// 切换类(有则删除,无则添加)
button.classList.toggle('active');
// 检查类
if (button.classList.contains('primary')) {
console.log('主要按钮');
}
// 替换类
button.classList.replace('old', 'new');
4. style 操作
element.style.property = 'value';
示例:
const box = document.querySelector('.box');
// 设置样式
box.style.color = 'red';
box.style.backgroundColor = 'blue';
box.style.fontSize = '20px';
// 获取样式
console.log(box.style.color); // red
// 移除样式
box.style.color = '';
// 使用 CSS 变量
box.style.setProperty('--main-color', '#007bff');
🎮 示例代码
示例 1:创建动态待办事项列表
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>待办事项列表</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.todo-item {
padding: 10px;
margin: 5px 0;
background: #f0f0f0;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.todo-item.completed {
background: #d4edda;
text-decoration: line-through;
}
button {
padding: 5px 10px;
cursor: pointer;
}
.delete-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 3px;
}
</style>
</head>
<body>
<h1>我的待办事项</h1>
<input type="text" id="todoInput" placeholder="输入待办事项...">
<button id="addBtn">添加</button>
<ul id="todoList"></ul>
<script>
const input = document.getElementById('todoInput');
const addBtn = document.getElementById('addBtn');
const todoList = document.getElementById('todoList');
// 添加待办事项
function addTodo() {
const text = input.value.trim();
if (text === '') {
alert('请输入待办事项!');
return;
}
// 创建列表项
const li = document.createElement('li');
li.className = 'todo-item';
// 创建文本
const span = document.createElement('span');
span.textContent = text;
li.appendChild(span);
// 创建完成按钮
const completeBtn = document.createElement('button');
completeBtn.textContent = '完成';
completeBtn.onclick = function() {
li.classList.toggle('completed');
};
li.appendChild(completeBtn);
// 创建删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function() {
li.remove();
updateCount();
};
li.appendChild(deleteBtn);
// 添加到列表
todoList.appendChild(li);
// 清空输入
input.value = '';
updateCount();
}
// 更新计数
function updateCount() {
const total = todoList.children.length;
const completed = todoList.querySelectorAll('.completed').length;
console.log(`总计: ${total}, 已完成: ${completed}`);
}
// 事件监听
addBtn.addEventListener('click', addTodo);
input.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
addTodo();
}
});
// 初始化示例数据
const examples = ['学习 DOM 操作', '完成练习任务', '复习 JavaScript'];
examples.forEach(text => {
input.value = text;
addTodo();
});
</script>
</body>
</html>
示例 2:动态创建表单
// 创建表单
function createDynamicForm() {
const form = document.createElement('form');
form.id = 'userForm';
// 用户名输入框
const usernameGroup = createFormGroup(
'username',
'text',
'用户名',
'请输入用户名'
);
form.appendChild(usernameGroup);
// 邮箱输入框
const emailGroup = createFormGroup(
'email',
'email',
'邮箱',
'请输入邮箱地址'
);
form.appendChild(emailGroup);
// 密码输入框
const passwordGroup = createFormGroup(
'password',
'password',
'密码',
'请输入密码'
);
form.appendChild(passwordGroup);
// 提交按钮
const submitBtn = document.createElement('button');
submitBtn.type = 'submit';
submitBtn.textContent = '提交';
submitBtn.className = 'submit-btn';
form.appendChild(submitBtn);
// 添加到页面
document.body.appendChild(form);
// 表单提交事件
form.addEventListener('submit', function(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
console.log('提交的数据:', { username, email, password });
alert('表单提交成功!');
});
}
// 创建表单组
function createFormGroup(id, type, labelText, placeholder) {
const div = document.createElement('div');
div.className = 'form-group';
const label = document.createElement('label');
label.htmlFor = id;
label.textContent = labelText;
div.appendChild(label);
const input = document.createElement('input');
input.type = type;
input.id = id;
input.name = id;
input.placeholder = placeholder;
input.required = true;
div.appendChild(input);
return div;
}
// 创建表单
createDynamicForm();
示例 3:图片画廊
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片画廊</title>
<style>
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
padding: 20px;
}
.gallery-item {
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.gallery-item img {
width: 100%;
height: 200px;
object-fit: cover;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.1);
}
.overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px;
transform: translateY(100%);
transition: transform 0.3s;
}
.gallery-item:hover .overlay {
transform: translateY(0);
}
</style>
</head>
<body>
<div id="gallery" class="gallery"></div>
<script>
// 图片数据
const images = [
{ url: 'https://via.placeholder.com/300x200/FF6B6B/ffffff?text=Image+1', title: '图片 1' },
{ url: 'https://via.placeholder.com/300x200/4ECDC4/ffffff?text=Image+2', title: '图片 2' },
{ url: 'https://via.placeholder.com/300x200/45B7D1/ffffff?text=Image+3', title: '图片 3' },
{ url: 'https://via.placeholder.com/300x200/FFA07A/ffffff?text=Image+4', title: '图片 4' },
{ url: 'https://via.placeholder.com/300x200/98D8C8/ffffff?text=Image+5', title: '图片 5' },
{ url: 'https://via.placeholder.com/300x200/F7DC6F/ffffff?text=Image+6', title: '图片 6' },
];
const gallery = document.getElementById('gallery');
// 创建画廊
function createGallery() {
images.forEach((image, index) => {
// 创建画廊项
const item = document.createElement('div');
item.className = 'gallery-item';
// 创建图片
const img = document.createElement('img');
img.src = image.url;
img.alt = image.title;
item.appendChild(img);
// 创建叠加层
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.textContent = image.title;
item.appendChild(overlay);
// 点击事件
item.addEventListener('click', function() {
showLightbox(image.url, image.title);
});
// 添加到画廊
gallery.appendChild(item);
});
}
// 显示灯箱
function showLightbox(url, title) {
// 创建灯箱遮罩
const lightbox = document.createElement('div');
lightbox.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
cursor: pointer;
`;
// 创建图片
const img = document.createElement('img');
img.src = url;
img.style.cssText = `
max-width: 90%;
max-height: 90%;
border-radius: 8px;
`;
lightbox.appendChild(img);
// 点击关闭
lightbox.addEventListener('click', function() {
lightbox.remove();
});
document.body.appendChild(lightbox);
}
// 初始化画廊
createGallery();
</script>
</body>
</html>
✍️ 练习任务
练习 1:实现选项卡切换
任务:创建一个选项卡组件,支持内容切换
要求:
- 创建 3 个选项卡(首页、关于、联系)
- 点击选项卡时切换显示内容
- 当前选项卡高亮显示
- 支持键盘导航(左右箭头)
提示:
<div class="tabs">
<button class="tab active" data-tab="home">首页</button>
<button class="tab" data-tab="about">关于</button>
<button class="tab" data-tab="contact">联系</button>
</div>
<div class="tab-content">
<div class="content active" id="home">首页内容</div>
<div class="content" id="about">关于内容</div>
<div class="content" id="contact">联系内容</div>
</div>
练习 2:实现模态框
任务:创建一个可复用的模态框组件
要求:
- 提供
openModal(title, content)函数 - 提供
closeModal()函数 - 支持点击遮罩层关闭
- 支持按 ESC 键关闭
- 添加动画效果(淡入淡出)
提示:
function openModal(title, content) {
// 创建遮罩层
// 创建模态框
// 添加到 body
// 绑定关闭事件
}
function closeModal() {
// 移除模态框
// 移除遮罩层
}
练习 3:实现星级评分
任务:创建一个交互式星级评分组件
要求:
- 显示 5 颗星
- 悬停时预览评分(高亮当前及之前的星)
- 点击时设置评分
- 显示当前评分数值
- 支持半星评分(可选)
提示:
const stars = document.querySelectorAll('.star');
stars.forEach((star, index) => {
// 悬停事件
star.addEventListener('mouseenter', () => {
highlightStars(index);
});
// 点击事件
star.addEventListener('click', () => {
setRating(index + 1);
});
});
🎓 今日挑战
挑战:实现完整的图片轮播组件
要求:
-
基本功能
- 自动播放(每 3 秒切换)
- 手动切换(上一张/下一张按钮)
- 指示器(显示当前位置)
- 无限循环(最后一张跳到第一张)
-
进阶功能
- 触摸滑动支持
- 键盘导航(左右箭头)
- 暂停/播放按钮
- 缩略图导航
-
动画效果
- 平滑过渡动画
- 淡入淡出或滑动效果
- 加载进度指示器
-
响应式设计
- 自适应不同屏幕尺寸
- 移动端优化
提示:
class Carousel {
constructor(container) {
this.container = container;
this.slides = container.querySelectorAll('.slide');
this.currentIndex = 0;
this.interval = null;
this.init();
}
init() {
// 初始化
this.showSlide(0);
this.startAutoPlay();
this.bindEvents();
}
showSlide(index) {
// 显示指定索引的幻灯片
// 更新指示器
}
next() {
// 显示下一张
}
prev() {
// 显示上一张
}
startAutoPlay() {
// 开始自动播放
}
stopAutoPlay() {
// 停止自动播放
}
bindEvents() {
// 绑定事件
}
}
扩展功能(可选):
- 懒加载图片
- 添加字幕
- 支持视频轮播
- 添加缩放功能
💡 常见问题
Q1: getElementById 和 querySelector 有什么区别?
A:
getElementById:
const elem = document.getElementById('myId');
- 只能通过 ID 查找
- 性能更好(直接查找)
- 返回单个元素或 null
querySelector:
const elem = document.querySelector('#myId');
- 支持任意 CSS 选择器
- 稍慢(需要解析选择器)
- 更灵活
选择建议:
- 只用 ID 时:
getElementById(更快) - 复杂选择器:
querySelector(更灵活)
Q2: innerHTML 和 textContent 有什么区别?
A:
innerHTML:
elem.innerHTML = '<strong>加粗</strong>';
// 显示:加粗(带格式)
- 解析 HTML
- 存在 XSS 风险
- 会重新创建 DOM 节点(性能开销大)
textContent:
elem.textContent = '<strong>加粗</strong>';
// 显示:<strong>加粗</strong>(纯文本)
- 纯文本,不解析 HTML
- 安全(无 XSS 风险)
- 性能更好
选择建议:
- 需要插入 HTML:
innerHTML(注意转义) - 只需文本:
textContent(安全快速)
Q3: HTMLCollection 和 NodeList 有什么区别?
A:
HTMLCollection:
const elems = document.getElementsByClassName('item');
- 实时更新(DOM 变化时自动更新)
getElementsByClassName,getElementsByTagName返回- 不是真正的数组(没有 forEach, map 等)
NodeList:
const elems = document.querySelectorAll('.item');
- 静态(DOM 变化时不更新)
querySelectorAll返回- 支持 forEach(现代浏览器)
示例:
// HTMLCollection(实时)
const items = document.getElementsByClassName('item');
console.log(items.length); // 3
document.body.appendChild(document.createElement('div')).className = 'item';
console.log(items.length); // 4(自动更新)
// NodeList(静态)
const items = document.querySelectorAll('.item');
console.log(items.length); // 3
document.body.appendChild(document.createElement('div')).className = 'item';
console.log(items.length); // 3(不变)
转换为数组:
// HTMLCollection 转 数组
const itemsArray = Array.from(document.getElementsByClassName('item'));
// NodeList 转 数组
const itemsArray = Array.from(document.querySelectorAll('.item'));
Q4: 如何避免 XSS 攻击?
A:
危险操作:
// ❌ 危险:用户输入直接插入
const userInput = '<img src=x onerror=alert(1)>';
elem.innerHTML = userInput;
安全方法:
- 使用 textContent:
// ✅ 安全
elem.textContent = userInput;
- 转义 HTML:
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 使用
const safe = escapeHtml(userInput);
elem.innerHTML = safe;
- 使用 DOM API:
// ✅ 安全:创建元素
const img = document.createElement('img');
img.src = sanitizeUrl(userUrl);
elem.appendChild(img);
- 使用 CSP(内容安全策略):
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
Q5: 如何优化 DOM 操作性能?
A:
优化技巧:
- 减少重排和重绘:
// ❌ 不好:多次修改
elem.style.width = '100px';
elem.style.height = '100px';
elem.style.background = 'red';
// ✅ 好:一次性修改
elem.style.cssText = 'width: 100px; height: 100px; background: red;';
- 批量操作:
// ❌ 不好:多次插入
for (let i = 0; i < 100; i++) {
document.body.appendChild(createElem());
}
// ✅ 好:使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
fragment.appendChild(createElem());
}
document.body.appendChild(fragment);
- 缓存查询结果:
// ❌ 不好:重复查询
for (let i = 0; i < 100; i++) {
document.getElementById('myId').style.opacity = i / 100;
}
// ✅ 好:缓存元素
const elem = document.getElementById('myId');
for (let i = 0; i < 100; i++) {
elem.style.opacity = i / 100;
}
- 使用事件委托:
// ❌ 不好:每个按钮都绑定
buttons.forEach(btn => {
btn.addEventListener('click', handler);
});
// ✅ 好:父元素委托
container.addEventListener('click', (e) => {
if (e.target.matches('button')) {
handler(e);
}
});
- 避免 layout thrashing:
// ❌ 不好:读写交错
elem1.style.height = elem1.offsetHeight + 10 + 'px'; // 读
elem2.style.height = elem2.offsetHeight + 10 + 'px'; // 读
// ✅ 好:先读后写
const h1 = elem1.offsetHeight; // 读
const h2 = elem2.offsetHeight; // 读
elem1.style.height = h1 + 10 + 'px'; // 写
elem2.style.height = h2 + 10 + 'px'; // 写
学习时间: 2026-03-09
难度: ⭐⭐⭐⭐☆
预计用时: 3-4 小时
关键词: DOM 操作, 元素查找, 内容修改, 事件处理, 性能优化