Day-10-DOM操作基础

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 中有几种常见的节点类型:

  1. 元素节点(Element):HTML 标签(如 <div>, <p>
  2. 文本节点(Text):元素内的文本内容
  3. 属性节点(Attribute):元素的属性(如 class, id
  4. 文档节点(Document):整个文档的根节点

示例

<p id="para1" class="text">这是一个段落</p>
  • 元素节点:<p>
  • 属性节点:id="para1", class="text"
  • 文本节点:"这是一个段落"

DOM 查找方式

查找元素是 DOM 操作的第一步,主要有以下方法:

  1. 通过 ID 查找getElementById()
  2. 通过类名查找getElementsByClassName()
  3. 通过标签名查找getElementsByTagName()
  4. 通过选择器查找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:实现选项卡切换

任务:创建一个选项卡组件,支持内容切换

要求

  1. 创建 3 个选项卡(首页、关于、联系)
  2. 点击选项卡时切换显示内容
  3. 当前选项卡高亮显示
  4. 支持键盘导航(左右箭头)

提示

<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:实现模态框

任务:创建一个可复用的模态框组件

要求

  1. 提供 openModal(title, content) 函数
  2. 提供 closeModal() 函数
  3. 支持点击遮罩层关闭
  4. 支持按 ESC 键关闭
  5. 添加动画效果(淡入淡出)

提示

function openModal(title, content) {
    // 创建遮罩层
    // 创建模态框
    // 添加到 body
    // 绑定关闭事件
}

function closeModal() {
    // 移除模态框
    // 移除遮罩层
}

练习 3:实现星级评分

任务:创建一个交互式星级评分组件

要求

  1. 显示 5 颗星
  2. 悬停时预览评分(高亮当前及之前的星)
  3. 点击时设置评分
  4. 显示当前评分数值
  5. 支持半星评分(可选)

提示

const stars = document.querySelectorAll('.star');

stars.forEach((star, index) => {
    // 悬停事件
    star.addEventListener('mouseenter', () => {
        highlightStars(index);
    });

    // 点击事件
    star.addEventListener('click', () => {
        setRating(index + 1);
    });
});

🎓 今日挑战

挑战:实现完整的图片轮播组件

要求

  1. 基本功能

    • 自动播放(每 3 秒切换)
    • 手动切换(上一张/下一张按钮)
    • 指示器(显示当前位置)
    • 无限循环(最后一张跳到第一张)
  2. 进阶功能

    • 触摸滑动支持
    • 键盘导航(左右箭头)
    • 暂停/播放按钮
    • 缩略图导航
  3. 动画效果

    • 平滑过渡动画
    • 淡入淡出或滑动效果
    • 加载进度指示器
  4. 响应式设计

    • 自适应不同屏幕尺寸
    • 移动端优化

提示

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;

安全方法

  1. 使用 textContent
// ✅ 安全
elem.textContent = userInput;
  1. 转义 HTML
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// 使用
const safe = escapeHtml(userInput);
elem.innerHTML = safe;
  1. 使用 DOM API
// ✅ 安全:创建元素
const img = document.createElement('img');
img.src = sanitizeUrl(userUrl);
elem.appendChild(img);
  1. 使用 CSP(内容安全策略)
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'">

Q5: 如何优化 DOM 操作性能?

A:

优化技巧

  1. 减少重排和重绘
// ❌ 不好:多次修改
elem.style.width = '100px';
elem.style.height = '100px';
elem.style.background = 'red';

// ✅ 好:一次性修改
elem.style.cssText = 'width: 100px; height: 100px; background: red;';
  1. 批量操作
// ❌ 不好:多次插入
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);
  1. 缓存查询结果
// ❌ 不好:重复查询
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;
}
  1. 使用事件委托
// ❌ 不好:每个按钮都绑定
buttons.forEach(btn => {
    btn.addEventListener('click', handler);
});

// ✅ 好:父元素委托
container.addEventListener('click', (e) => {
    if (e.target.matches('button')) {
        handler(e);
    }
});
  1. 避免 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 操作, 元素查找, 内容修改, 事件处理, 性能优化