Appearance
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 返回单个元素或 null。querySelector 用 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 中是保留字,属性名用 classNameclassList
更方便的类名操作方式:
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
});
}