DOM 查询与关系查找
上一节用 querySelector() 找到 <h2> 和 <span>,把班级名称和平均分写到了页面。现在学员列表有三张卡片:需要数出卡片、找到待审核的李四,并从姓名找到所属卡片。页面结构不完整时,还要知道哪个元素没有找到。
完成这些任务,先要准确查询元素。本节只学习怎样选中一个或多个元素、沿 DOM 关系继续查找,以及查询不到时怎样排错。
元素内容、样式、属性和 data-* 的修改统一放在下一节。
基本查询
DOM 树
<section id="student-panel">
<h2>学员列表</h2>
<ul class="student-list">
<li class="student-item">张三</li>
<li class="student-item">李四</li>
</ul>
</section>浏览器把标签转换为有层级关系的对象:
section 是 h2 和 ul 的父元素,两个 li 是 ul 的子元素,也是彼此的兄弟元素。
单个元素查询
querySelector:查询第一个元素
<h1 id="page-title">学员管理</h1>
<p class="summary">共 3 名学员</p>const pageTitle = document.querySelector("#page-title");
const summary = document.querySelector(".summary");
console.log(pageTitle);
console.log(summary);querySelector() 接收 CSS 选择器字符串,返回第一个匹配元素;没有匹配时返回 null。
document.querySelector("#page-title"); // ID
document.querySelector(".summary"); // class
document.querySelector("h1"); // 元素多个元素匹配时仍只返回第一个:
<p class="message">第一条提示</p>
<p class="message">第二条提示</p>const firstMessage = document.querySelector(".message");
console.log(firstMessage.textContent); // 第一条提示这里读取 textContent 只是为了确认查到了谁,内容修改在下一节展开。
限定查询范围
<section id="student-panel">
<h2 class="panel-title">学员列表</h2>
</section>
<section id="notice-panel">
<h2 class="panel-title">课程通知</h2>
</section>const studentPanel = document.querySelector("#student-panel");
if (studentPanel) {
const studentTitle = studentPanel.querySelector(".panel-title");
console.log(studentTitle?.textContent);
}先选中业务区域,再在区域内部查询,可以避免同名类相互影响,也让代码职责更清楚。
多个元素查询
querySelectorAll:查询多个元素
<ul>
<li class="student-item">张三</li>
<li class="student-item">李四</li>
<li class="student-item">王五</li>
</ul>const studentItems = document.querySelectorAll(".student-item");
console.log(studentItems);
console.log(studentItems.length); // 3
console.log(studentItems[0]); // 第一个 li返回值是静态 NodeList。它不是单个元素,也不是普通数组,但可以按索引访问并遍历。
studentItems.forEach((item, index) => {
console.log(index, item.textContent);
});
for (const item of studentItems) {
console.log(item.textContent);
}如果需要数组方法,可以显式转换:
const itemArray = [...studentItems];
const studentNames = itemArray.map((item) => item.textContent.trim());
console.log(studentNames);静态 NodeList
querySelectorAll() 查询完成后,结果不会随着页面元素增删自动更新。打开本节的学员列表结构查询页面(源码文件:./.code/p04/c02-selector/04-selector-inspection.html),在 Console 中执行:
const firstQuery = document.querySelectorAll(".student-card");
console.log(firstQuery.length); // 3接着在 Elements 面板中,临时复制一张 .student-card 卡片并放进 #student-list。回到 Console 重新查询:
const latestQuery = document.querySelectorAll(".student-card");
console.log(firstQuery.length, latestQuery.length); // 3 4firstQuery 保留查询时的 3 张卡片,latestQuery 得到现在的 4 张。刷新页面可以恢复原结构。JavaScript 创建和删除元素留到 p04c04;当前先记住:需要最新结果就重新查询。
CSS 选择器速查
基础与组合选择器
| 需求 | 查询示例 |
|---|---|
| 第一个按钮 | document.querySelector("button") |
| 指定 ID | document.querySelector("#search-button") |
| 第一个指定类 | document.querySelector(".student-item") |
| 输入框和下拉框 | document.querySelectorAll("input, select") |
| 主按钮 | document.querySelectorAll("button.primary") |
层级选择器
| 需求 | 查询示例 |
|---|---|
| 面板内所有学员项 | #student-panel .student-item |
| 列表直接子元素 | #student-list > li |
| 选中项后的第一个兄弟 | .selected + li |
| 选中项后的全部兄弟 | .selected ~ li |
属性与结构选择器
| 需求 | 查询示例 |
|---|---|
有 data-id 属性 | [data-id] |
| 属性值为 true | [data-disabled="true"] |
| 第一项 | li:first-child |
| 最后一项 | li:last-child |
| 第二项 | li:nth-child(2) |
:first-child 和 :nth-child() 按同一父元素下的所有元素子项判断位置,不是按整页的 li 顺序。nth-child() 从 1 开始:
const secondItem = document.querySelector(
"#student-list > li:nth-child(2)",
);属性选择器可以定位元素,本节不通过 dataset 读取或修改属性。
元素关系查找
| 属性或方法 | 说明 |
|---|---|
parentElement | 直接父元素 |
children | 所有直接子元素 |
firstElementChild | 第一个子元素 |
lastElementChild | 最后一个子元素 |
previousElementSibling | 前一个兄弟元素 |
nextElementSibling | 后一个兄弟元素 |
closest(selector) | 从当前元素向上查找最近的匹配元素 |
matches(selector) | 判断当前元素是否匹配选择器 |
父子与兄弟关系
父子元素
<article class="student-card">
<h3 class="student-name">张三</h3>
<p class="student-class">FastAPI Web 1 班</p>
</article>const studentName = document.querySelector(".student-name");
if (studentName) {
const studentCard = studentName.parentElement;
console.log(studentCard);
console.log(studentCard?.children);
console.log(studentCard?.firstElementChild);
console.log(studentCard?.lastElementChild);
}children 只包含元素,不包含 HTML 缩进和换行产生的文本节点,适合当前课程使用。
DOM 中的节点(Node)还包括文本、注释等;元素(Element)是带标签的那一类节点。因此本节优先使用名称中带 Element 的关系属性。
集合也要区分来源:
| 查询结果 | 类型与变化方式 |
|---|---|
querySelectorAll() | 静态 NodeList,保留查询当时的结果 |
element.children | 动态 HTMLCollection,随直接子元素增删而变化 |
两者都不是数组,需要数组方法时先使用 Array.from()。p04c04 开始增删节点时,若要遍历并删除一批元素,可以先转成数组固定本次处理范围,避免动态集合边遍历边变化。
兄弟元素
<button>上一个</button>
<button id="current-button">当前</button>
<button>下一个</button>const currentButton = document.querySelector("#current-button");
console.log(currentButton?.previousElementSibling);
console.log(currentButton?.nextElementSibling);当前元素已经位于首尾时,对应结果为 null。
closest() 与 matches()
向上查找:closest()
<article class="student-card">
<div class="student-info">
<strong class="student-name">张三</strong>
</div>
</article>const studentName = document.querySelector(".student-name");
const studentCard = studentName?.closest(".student-card");
console.log(studentCard);closest() 包含当前元素自身,再逐层向上查询。后面的事件委托会经常使用它。
匹配判断:matches()
const firstItem = document.querySelector(".student-item");
console.log(firstItem?.matches("li.student-item"));matches() 不继续查找其他元素,只回答当前元素是否符合选择器。
查询失败排查
下面的代码在查询失败后继续读取属性,会报错:
const saveButton = document.querySelector("#save-button");
console.log(saveButton.textContent);常见提示:
Cannot read properties of null按顺序排查:
- 在 Elements 中确认元素是否存在。
- 检查 ID、class 和选择器拼写。
- 检查
#、.是否遗漏。 - 检查查询范围是否过窄。
- 检查脚本是否使用
defer或在 DOM 创建之后执行。 - 确认元素是否位于另一个页面或 iframe。
关键结构可以主动检查:
const saveButton = document.querySelector("#save-button");
if (!saveButton) {
throw new Error("没有找到 #save-button,请检查页面结构");
}可选元素则通过条件判断或可选链处理,不需要一律抛错。
综合案例:学员列表检查
案例只查询和输出,不修改内容、class 或属性。
<section id="student-panel">
<h1>学员列表</h1>
<div id="student-list">
<article class="student-card" data-status="active">
<h2 class="student-name">张三</h2>
<p class="student-course">FastAPI Web 1 班</p>
</article>
<article class="student-card" data-status="pending">
<h2 class="student-name">李四</h2>
<p class="student-course">Python 基础 2 班</p>
</article>
<article class="student-card" data-status="disabled">
<h2 class="student-name">王五</h2>
<p class="student-course">FastAPI Web 1 班</p>
</article>
</div>
</section>const studentPanel = document.querySelector("#student-panel");
const studentList = studentPanel?.querySelector("#student-list");
const studentCards = studentList?.querySelectorAll(".student-card");
const pendingCard = studentList?.querySelector(
'[data-status="pending"]',
);
const firstCard = studentList?.firstElementChild;
const lastCard = studentList?.lastElementChild;
if (!studentPanel || !studentList || !studentCards) {
throw new Error("学员列表结构不完整");
}
console.log("学员数量:", studentCards.length);
console.log("第一张卡片:", firstCard);
console.log("最后一张卡片:", lastCard);
console.log("待审核卡片:", pendingCard);
studentCards.forEach((card, index) => {
const name = card.querySelector(".student-name");
const course = card.querySelector(".student-course");
console.log(`${index + 1}. ${name?.textContent} · ${course?.textContent}`);
});
const firstName = studentList.querySelector(".student-name");
console.log("从姓名向上找到卡片:", firstName?.closest(".student-card"));
console.log("第二张卡片:", firstCard?.nextElementSibling);代码使用了范围查询、单个/多个查询、属性选择器、父子关系、兄弟关系和 closest(),但没有提前承担下一节的修改职责。
运行与验收
直接打开学员列表结构查询页面(源码文件:./.code/p04/c02-selector/04-selector-inspection.html),在 Console 中核对:学员数量为 3;待审核卡片和第二张卡片都是李四;依次输出张三、李四、王五;首尾卡片分别是张三和王五,从张三姓名向上找到的也是张三所在卡片。页面上的学员信息不会被脚本改动。
完整附件:学员列表结构查询
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM 元素查询:检查学员列表结构</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: #f4f7fb;
color: #273142;
font-family: Arial, "Microsoft YaHei", sans-serif;
}
.student-panel {
width: min(860px, calc(100% - 32px));
margin: 40px auto;
}
.student-panel > p {
color: #667085;
}
.student-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.student-card {
flex: 1 1 220px;
padding: 20px;
border: 1px solid #d9e1ec;
border-radius: 10px;
background-color: #ffffff;
}
.student-name {
margin-top: 0;
color: #17233d;
}
.student-course {
margin-bottom: 0;
color: #667085;
}
</style>
</head>
<body>
<section id="student-panel" class="student-panel">
<h1>学员列表</h1>
<p>页面不修改任何数据,请在 Console 查看查询结果。</p>
<div id="student-list" class="student-list">
<article class="student-card" data-status="active">
<h2 class="student-name">张三</h2>
<p class="student-course">FastAPI Web 1 班</p>
</article>
<article class="student-card" data-status="pending">
<h2 class="student-name">李四</h2>
<p class="student-course">Python 基础 2 班</p>
</article>
<article class="student-card" data-status="disabled">
<h2 class="student-name">王五</h2>
<p class="student-course">FastAPI Web 1 班</p>
</article>
</div>
</section>
<script>
const studentPanel = document.querySelector("#student-panel");
const studentList = studentPanel?.querySelector("#student-list");
const studentCards = studentList?.querySelectorAll(".student-card");
const pendingCard = studentList?.querySelector(
'[data-status="pending"]',
);
const firstCard = studentList?.firstElementChild;
const lastCard = studentList?.lastElementChild;
if (!studentPanel || !studentList || !studentCards) {
throw new Error("学员列表结构不完整");
}
console.log("学员数量:", studentCards.length);
console.log("第一张卡片:", firstCard);
console.log("最后一张卡片:", lastCard);
console.log("待审核卡片:", pendingCard);
studentCards.forEach((card, index) => {
const name = card.querySelector(".student-name");
const course = card.querySelector(".student-course");
console.log(
`${index + 1}. ${name?.textContent} · ${course?.textContent}`,
);
});
const firstName = studentList.querySelector(".student-name");
console.log(
"从姓名向上找到卡片:",
firstName?.closest(".student-card"),
);
console.log("第二张卡片:", firstCard?.nextElementSibling);
</script>
</body>
</html>小结
querySelector()返回第一个元素或null。querySelectorAll()返回静态NodeList,可按索引访问和遍历。- CSS 选择器可以直接复用于 DOM 查询。
- 范围查询可减少同名元素冲突。
- 父子、兄弟、
closest()与matches()能从当前元素继续查找。