JavaScript DOM 操作

DOM(Document Object Model,文档对象模型)是浏览器把 HTML 解析后生成的"对象树"。JS 通过 DOM API 可以读取、修改、增删页面元素——这就是"动态网页"的本质。这一章我们学习怎么用 JS 操作 DOM。

1. 什么是 DOM

浏览器加载 HTML 时,会把它解析成一棵由节点组成的树。比如:

<!-- HTML -->
<html>
  <head><title>页面</title></head>
  <body>
    <div id="main">
      <p>段落</p>
    </div>
  </body>
</html>

<!-- 对应的 DOM 树(简化):
        document
           |
         html
        /    \
     head    body
      |       |
    title    div#main
      |       |
    "页面"     p
              |
            "段落"  -->

JS 通过 document 这个全局对象访问整棵树。window.document 是它的全称。

2. 选中元素(querySelector)

操作 DOM 的第一步是拿到元素。现代 JS 用 querySelector 系列,语法和 CSS 选择器完全一样:

<!-- 假设页面有这些元素 -->
<button id="btn">点我</button>
<div class="item">第一项</div>
<div class="item">第二项</div>
<input name="username" />

<script>
// 1. querySelector:CSS 选择器,返回第一个匹配
const btn = document.querySelector("#btn");
const item = document.querySelector(".item");
const input = document.querySelector("input[name=username]");

// 2. querySelectorAll:返回所有匹配(NodeList)
const items = document.querySelectorAll(".item");
console.log(items.length);   // 2
items.forEach(el => console.log(el.textContent));

// 3. 老 API(也能用,但不如 querySelector 直观)
const btn2 = document.getElementById("btn");
const items2 = document.getElementsByClassName("item");   // HTMLCollection
const buttons = document.getElementsByTagName("button");
</script>

选择器优先级:

注意:getElementsByClassName 返回的是动态的 HTMLCollection(DOM 改了它就跟着变),querySelectorAll 返回静态的 NodeList。新项目统一用后者。

3. 修改内容

const el = document.querySelector("#title");

// 1. textContent:纯文本(推荐!安全、快)
el.textContent = "新标题";

// 2. innerHTML:可以包含 HTML 标签(注意 XSS 风险)
el.innerHTML = "<strong>新标题</strong>";

// 3. 区别演示
const userInput = "<img src=x onerror=alert('XSS')>";
el.textContent = userInput;   // ✅ 安全:原样显示文本
// el.innerHTML = userInput;  // ❌ 危险:会执行里面的脚本!

// 读取内容
console.log(el.textContent);   // 读取纯文本
console.log(el.innerHTML);     // 读取 HTML

// hidden 属性:显示/隐藏
el.hidden = true;     // 等价 display: none
el.hidden = false;

XSS 警告:innerHTML 会解析 HTML 字符串,如果内容来自用户输入,可能被注入恶意脚本(经典 XSS 攻击)。规则是——凡是用户输入的内容,一律用 textContent,不要拼到 innerHTML

4. 修改属性

const img = document.querySelector("img");

// 1. 标准 attribute:直接用属性
console.log(img.src);          // 读取
img.alt = "图片描述";          // 设置
console.log(img.id);

// 2. 任意 attribute:getAttribute / setAttribute
console.log(img.getAttribute("data-id"));
img.setAttribute("data-id", "100");
img.removeAttribute("title");

// 3. dataset:操作 data-* 属性(超常用)
const user = document.querySelector("[data-user-id]");
user.dataset.userId = "200";       // HTML 里是 data-user-id
console.log(user.dataset.userId);  // "200"(自动驼峰)

// 4. class 操作:用 classList,不要直接改 className
const el = document.querySelector(".box");
el.classList.add("active");          // 添加
el.classList.remove("box");          // 删除
el.classList.toggle("dark");         // 切换:有则删,无则加
el.classList.contains("active");     // 是否包含(返回布尔)
el.classList.replace("old", "new");  // 替换

dataset 是处理 data-* 自定义属性的标准方式——HTML 里写 data-user-id,JS 里读 el.dataset.userId(自动转驼峰)。在列表项上存数据、点击时取出,这种模式很常用。

5. 修改样式

const el = document.querySelector("#box");

// 1. 单独改 style:语法 el.style.属性名
el.style.color = "red";
el.style.fontSize = "20px";      // 注意:CSS 是 font-size,JS 用驼峰
el.style.backgroundColor = "#f0f0f0";
el.style.display = "none";       // 隐藏

// 读取计算样式(只读)
const computed = getComputedStyle(el);
console.log(computed.height);    // "200px"

// 2. 推荐:用 class 切换样式,而不是改 style
// CSS:
//   .highlight { background: yellow; }
//   .hidden { display: none; }
el.classList.add("highlight");
el.classList.add("hidden");

// 3. 改多个样式:批量设置
Object.assign(el.style, {
  color: "white",
  background: "blue",
  padding: "10px 20px"
});

最佳实践:用 class 切换样式,不要直接改 style。原因:CSS 和 JS 解耦,主题切换、动画、维护都更方便。直接改 style.xxx 只适合动态计算的值(如拖拽位置、图表宽度)。

6. 创建与插入元素

// 1. 创建元素
const p = document.createElement("p");
p.textContent = "我是新段落";
p.classList.add("msg");

// 2. 插入到 DOM 树
document.body.appendChild(p);          // 加到 body 末尾

// 插入到指定容器
const container = document.querySelector("#list");
container.appendChild(p);

// 3. 更现代的 API:append / prepend / before / after
container.append(p);                   // 末尾加,可传多个
container.prepend(p);                  // 开头加
container.before(p);                   // 元素前面加
container.after(p);                    // 元素后面加

// 4. 删除元素
p.remove();                            // 直接删(推荐)
// 老写法
// p.parentNode.removeChild(p);

// 5. 克隆
const clone = p.cloneNode(true);       // true 表示深克隆(含子节点)

现代 API append/prepend/before/after 比老的 appendChild/insertBefore 更直观,而且支持插入多个参数和文本节点。新项目优先用现代 API。

7. 批量构建列表

从接口拿到一组数据,渲染成列表/表格,是前端最高频的操作:

// 批量创建列表项(常见场景)

// ❌ 老写法:用字符串拼接(易出错、有 XSS 风险)
const html = users.map(u => `<li>${u.name}</li>`).join("");
ul.innerHTML = html;

// ✅ 推荐:用 DOM API
const ul = document.querySelector("#list");
const users = [{ name: "小明" }, { name: "小红" }];
users.forEach(u => {
  const li = document.createElement("li");
  li.textContent = u.name;
  ul.appendChild(li);
});

// ✅ 性能优化:用 DocumentFragment 批量插入
const frag = document.createDocumentFragment();
users.forEach(u => {
  const li = document.createElement("li");
  li.textContent = u.name;
  frag.appendChild(li);
});
ul.appendChild(frag);     // 只触发一次重排

性能要点:DOM 操作是"重活",每次插入都触发重排(reflow)重绘(repaint)。批量插入时一定要用 DocumentFragment(或字符串模板一次性 innerHTML),避免在循环里反复 append。

8. 表单读写

// 读取表单值
const input = document.querySelector("#username");
console.log(input.value);       // 输入框的值

const checkbox = document.querySelector("#agree");
console.log(checkbox.checked);  // 布尔,是否勾选

const select = document.querySelector("#city");
console.log(select.value);      // 选中的 value

// 设置值
input.value = "默认名字";
checkbox.checked = true;

// 监听表单提交
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault();             // 阻止默认提交刷新页面
  const data = new FormData(form);
  console.log(Object.fromEntries(data));  // { username: "...", ... }
});

FormData 是处理表单的现代方案——一次性拿到所有字段的键值对,无需手动遍历。配合 Object.fromEntries 直接转成对象,提交 AJAX 时极其方便。

9. 实战:待办列表

<input id="input" placeholder="输入待办" />
<button id="add">添加</button>
<ul id="list"></ul>

<script>
  const input = document.querySelector("#input");
  const addBtn = document.querySelector("#add");
  const list = document.querySelector("#list");

  addBtn.addEventListener("click", () => {
    const text = input.value.trim();
    if (!text) return;

    const li = document.createElement("li");
    li.textContent = text;             // 用 textContent 防 XSS

    // 添加删除按钮
    const delBtn = document.createElement("button");
    delBtn.textContent = "删除";
    delBtn.addEventListener("click", () => li.remove());
    li.appendChild(delBtn);

    list.appendChild(li);
    input.value = "";                  // 清空输入框
    input.focus();
  });
</script>

这个例子融合了选元素、读输入、创建节点、添加事件、删除节点——是 DOM 操作的"全家桶"练习。把它写出来跑通,DOM 就基本入门了。

10. 性能建议

小结

下一篇我们看事件——让网页真正"响应"用户的操作。

← 上一篇 JavaScript 字符串

下一篇 JavaScript 事件

✈️💬