Skip to content

16|DOM 查询、修改与事件监听

HTML 和 CSS 构建出静态页面,JavaScript 通过 DOM(Document Object Model) API 让页面动起来:读取用户输入、修改元素内容、响应点击事件。DOM 把 HTML 文档表示为一棵树,JavaScript 可以查询树上的节点、修改节点属性、监听用户交互。

一、查询元素

单个元素

js
// 通过 ID(最常用,效率最高)
const el = document.getElementById("server-list");

// 通过 CSS 选择器(更灵活)
const el2 = document.querySelector(".container .item");   // 匹配的第一个元素

getElementById 返回单个元素或 nullquerySelector 用 CSS 选择器语法,返回匹配的第一个元素。

多个元素

js
// 通过类名(返回 HTMLCollection,实时更新)
const items = document.getElementsByClassName("item");

// 通过标签名
const divs = document.getElementsByTagName("div");

// 通过 CSS 选择器(返回 NodeList,静态快照)
const rows = document.querySelectorAll(".table-row");

querySelectorAll 返回的 NodeList 可以用 forEach 遍历,但不是真正的数组,没有 map/filter

js
// 可以
rows.forEach((row) => {
  console.log(row.textContent);
});

// 不可以
// rows.map(...);   // TypeError: rows.map is not a function

// 转为真数组
const arr = Array.from(rows);
// 或 const arr = [...rows];

二、读取和修改内容

textContent

读取或设置元素的纯文本内容:

js
const title = document.getElementById("title");
console.log(title.textContent);   // "运维平台"

title.textContent = "服务器管理";   // 替换为纯文本,HTML 标签会被转义

innerHTML

读取或设置元素的 HTML 内容:

js
const container = document.getElementById("list");
container.innerHTML = "<li>nginx</li><li>redis</li>";

innerHTML 会解析字符串中的 HTML 标签。永远不要直接把用户输入插入到 innerHTML,这会导致 XSS(跨站脚本攻击):

js
// 危险:用户输入可能包含恶意脚本
container.innerHTML = userInput;

// 安全:用 textContent,恶意标签会被转义为纯文本
container.textContent = userInput;

表单值

js
const input = document.getElementById("hostname");
console.log(input.value);     // 读取输入框的值
input.value = "web-01";       // 设置输入框的值

const checkbox = document.getElementById("active");
console.log(checkbox.checked);   // true / false

属性

js
const link = document.getElementById("docs");

// 读取属性
console.log(link.getAttribute("href"));

// 设置属性
link.setAttribute("href", "https://example.com/docs");
link.setAttribute("target", "_blank");

// 删除属性
link.removeAttribute("target");

// 标准属性可以直接用点语法
link.href = "https://example.com";
link.className = "external-link";   // 注意:class 在 JS 中是保留字,属性名用 className

classList

更方便的类名操作方式:

js
const el = document.getElementById("status");

el.classList.add("active");       // 添加类
el.classList.remove("active");    // 移除类
el.classList.toggle("active");    // 有则移除,无则添加
el.classList.contains("active");  // 判断是否包含(返回 true/false)

三、创建和插入元素

js
// 创建新元素
const li = document.createElement("li");
li.textContent = "mysql";
li.classList.add("service-item");

// 插入到父元素末尾
const list = document.getElementById("service-list");
list.appendChild(li);

// 插入到指定元素之前
const firstItem = list.querySelector("li");
list.insertBefore(li, firstItem);

// 现代方法(更灵活)
list.append(li);           // 末尾添加(可多个)
list.prepend(li);          // 开头添加
firstItem.before(li);      // 在前面插入
firstItem.after(li);       // 在后面插入

文档片段(DocumentFragment)

批量插入元素时,用 DocumentFragment 减少 DOM 重绘次数:

js
const fragment = document.createDocumentFragment();

const services = ["nginx", "redis", "mysql"];
services.forEach((name) => {
  const li = document.createElement("li");
  li.textContent = name;
  fragment.appendChild(li);
});

// 只触发一次 DOM 更新
document.getElementById("list").appendChild(fragment);

四、事件监听

绑定事件

js
const btn = document.getElementById("restart-btn");

btn.addEventListener("click", function (event) {
  console.log("按钮被点击");
  console.log(event.target);    // 触发事件的元素
});

addEventListener 的参数:

参数含义
事件类型"click""submit""input""change"
回调函数事件触发时执行,接收 Event 对象

常用事件

事件触发时机
click元素被点击
dblclick元素被双击
submit表单提交
input输入框值变化(每次输入都触发)
change值变化且失去焦点后触发(select、checkbox 等是变化即触发)
focus / blur元素获得/失去焦点
keydown / keyup键盘按下/释放
mouseover / mouseout鼠标进入/离开元素

事件对象

js
btn.addEventListener("click", (event) => {
  event.preventDefault();     // 阻止默认行为(如链接跳转、表单提交)
  event.stopPropagation();    // 阻止事件冒泡
  console.log(event.clientX, event.clientY);   // 鼠标点击坐标
});

事件冒泡

点击子元素时,事件会从子元素向上传播到父元素:

html
<div id="outer">
  <button id="inner">点击</button>
</div>
js
document.getElementById("outer").addEventListener("click", () => {
  console.log("outer 被点击");
});

document.getElementById("inner").addEventListener("click", (e) => {
  console.log("inner 被点击");
  e.stopPropagation();   // 阻止冒泡,outer 不会触发
});

事件委托

如果列表项很多,给每个项单独绑定事件监听器效率低。利用事件冒泡,在父元素上统一监听:

js
document.getElementById("server-list").addEventListener("click", (event) => {
  // 判断点击的是否是列表项
  if (event.target.tagName === "LI") {
    console.log("点击了:", event.target.textContent);
  }
});

事件委托的优势:

  • 只需一个监听器,内存占用低
  • 动态新增的元素自动生效,不需要重新绑定

五、移除元素

js
const el = document.getElementById("old-item");
el.remove();   // 从 DOM 中移除自身

// 旧写法(兼容老浏览器)
// el.parentNode.removeChild(el);

六、常见错误

在 DOM 加载前查询元素

js
// 错误:脚本在 body 前执行,此时元素还不存在
const el = document.getElementById("app");   // null

// 解决 1:把 script 放在 body 末尾
// 解决 2:用 DOMContentLoaded 事件
document.addEventListener("DOMContentLoaded", () => {
  const el = document.getElementById("app");   // 正常
});

用 innerHTML 插入用户输入

js
// 危险:XSS
container.innerHTML = userComment;

// 安全:用 textContent 或手动创建元素
const p = document.createElement("p");
p.textContent = userComment;
container.appendChild(p);

忘记移除事件监听器

js
// 错误:重复绑定导致回调执行多次
function update() { /* ... */ }
btn.addEventListener("click", update);
btn.addEventListener("click", update);   // 又绑定了一次

// 正确:确保只绑定一次,或在不需要时移除
btn.removeEventListener("click", update);

用 for 循环绑定事件时捕获变量

js
// 错误:所有按钮都输出最后一个 i 的值
for (var i = 0; i < 3; i++) {
  btns[i].addEventListener("click", () => {
    console.log(i);   // 总是输出 3
  });
}

// 正确:用 let(块级作用域),或用事件委托
for (let i = 0; i < 3; i++) {
  btns[i].addEventListener("click", () => {
    console.log(i);   // 输出 0, 1, 2
  });
}