Skip to content

DOM 查询与关系查找 ​

上一节用 querySelector() 找到 <h2> 和 <span>,把班级名称和平均分写到了页面。现在学员列表有三张卡片:需要数出卡片、找到待审核的李四,并从姓名找到所属卡片。页面结构不完整时,还要知道哪个元素没有找到。

完成这些任务,先要准确查询元素。本节只学习怎样选中一个或多个元素、沿 DOM 关系继续查找,以及查询不到时怎样排错。

元素内容、样式、属性和 data-* 的修改统一放在下一节。

基本查询 ​

DOM 树 ​

html

<section id="student-panel">
    <h2>学员列表</h2>
    <ul class="student-list">
        <li class="student-item">张三</li>
        <li class="student-item">李四</li>
    </ul>
</section>

浏览器把标签转换为有层级关系的对象:

mermaid Diagram

section 是 h2 和 ul 的父元素,两个 li 是 ul 的子元素,也是彼此的兄弟元素。

单个元素查询 ​

querySelector:查询第一个元素

html
<h1 id="page-title">学员管理</h1>
<p class="summary">共 3 名学员</p>
js
const pageTitle = document.querySelector("#page-title");
const summary = document.querySelector(".summary");

console.log(pageTitle);
console.log(summary);

querySelector() 接收 CSS 选择器字符串,返回第一个匹配元素;没有匹配时返回 null。

js
document.querySelector("#page-title"); // ID
document.querySelector(".summary");    // class
document.querySelector("h1");          // 元素

多个元素匹配时仍只返回第一个:

html
<p class="message">第一条提示</p>
<p class="message">第二条提示</p>
js
const firstMessage = document.querySelector(".message");
console.log(firstMessage.textContent); // 第一条提示

这里读取 textContent 只是为了确认查到了谁,内容修改在下一节展开。

限定查询范围 ​

html

<section id="student-panel">
    <h2 class="panel-title">学员列表</h2>
</section>

<section id="notice-panel">
    <h2 class="panel-title">课程通知</h2>
</section>
js
const studentPanel = document.querySelector("#student-panel");

if (studentPanel) {
    const studentTitle = studentPanel.querySelector(".panel-title");
    console.log(studentTitle?.textContent);
}

先选中业务区域,再在区域内部查询,可以避免同名类相互影响,也让代码职责更清楚。

多个元素查询 ​

querySelectorAll:查询多个元素

html

<ul>
    <li class="student-item">张三</li>
    <li class="student-item">李四</li>
    <li class="student-item">王五</li>
</ul>
js
const studentItems = document.querySelectorAll(".student-item");

console.log(studentItems);
console.log(studentItems.length); // 3
console.log(studentItems[0]);     // 第一个 li

返回值是静态 NodeList。它不是单个元素,也不是普通数组,但可以按索引访问并遍历。

js
studentItems.forEach((item, index) => {
    console.log(index, item.textContent);
});

for (const item of studentItems) {
    console.log(item.textContent);
}

如果需要数组方法,可以显式转换:

js
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 中执行:

js
const firstQuery = document.querySelectorAll(".student-card");
console.log(firstQuery.length); // 3

接着在 Elements 面板中,临时复制一张 .student-card 卡片并放进 #student-list。回到 Console 重新查询:

js
const latestQuery = document.querySelectorAll(".student-card");
console.log(firstQuery.length, latestQuery.length); // 3 4

firstQuery 保留查询时的 3 张卡片,latestQuery 得到现在的 4 张。刷新页面可以恢复原结构。JavaScript 创建和删除元素留到 p04c04;当前先记住:需要最新结果就重新查询。

CSS 选择器速查 ​

基础与组合选择器

需求查询示例
第一个按钮document.querySelector("button")
指定 IDdocument.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 开始:

js
const secondItem = document.querySelector(
    "#student-list > li:nth-child(2)",
);

属性选择器可以定位元素,本节不通过 dataset 读取或修改属性。

元素关系查找 ​

属性或方法说明
parentElement直接父元素
children所有直接子元素
firstElementChild第一个子元素
lastElementChild最后一个子元素
previousElementSibling前一个兄弟元素
nextElementSibling后一个兄弟元素
closest(selector)从当前元素向上查找最近的匹配元素
matches(selector)判断当前元素是否匹配选择器

父子与兄弟关系 ​

父子元素

html

<article class="student-card">
    <h3 class="student-name">张三</h3>
    <p class="student-class">FastAPI Web 1 班</p>
</article>
js
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 开始增删节点时,若要遍历并删除一批元素,可以先转成数组固定本次处理范围,避免动态集合边遍历边变化。

兄弟元素

html

<button>上一个</button>
<button id="current-button">当前</button>
<button>下一个</button>
js
const currentButton = document.querySelector("#current-button");

console.log(currentButton?.previousElementSibling);
console.log(currentButton?.nextElementSibling);

当前元素已经位于首尾时,对应结果为 null。

closest() 与 matches() ​

向上查找:closest()

html

<article class="student-card">
    <div class="student-info">
        <strong class="student-name">张三</strong>
    </div>
</article>
js
const studentName = document.querySelector(".student-name");
const studentCard = studentName?.closest(".student-card");

console.log(studentCard);

closest() 包含当前元素自身,再逐层向上查询。后面的事件委托会经常使用它。

匹配判断:matches()

js
const firstItem = document.querySelector(".student-item");

console.log(firstItem?.matches("li.student-item"));

matches() 不继续查找其他元素,只回答当前元素是否符合选择器。

查询失败排查 ​

下面的代码在查询失败后继续读取属性,会报错:

js
const saveButton = document.querySelector("#save-button");
console.log(saveButton.textContent);

常见提示:

text
Cannot read properties of null

按顺序排查:

  1. 在 Elements 中确认元素是否存在。
  2. 检查 ID、class 和选择器拼写。
  3. 检查 #、. 是否遗漏。
  4. 检查查询范围是否过窄。
  5. 检查脚本是否使用 defer 或在 DOM 创建之后执行。
  6. 确认元素是否位于另一个页面或 iframe。

关键结构可以主动检查:

js
const saveButton = document.querySelector("#save-button");

if (!saveButton) {
    throw new Error("没有找到 #save-button,请检查页面结构");
}

可选元素则通过条件判断或可选链处理,不需要一律抛错。

综合案例:学员列表检查 ​

案例只查询和输出,不修改内容、class 或属性。

html

<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>
js
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;待审核卡片和第二张卡片都是李四;依次输出张三、李四、王五;首尾卡片分别是张三和王五,从张三姓名向上找到的也是张三所在卡片。页面上的学员信息不会被脚本改动。

完整附件:学员列表结构查询
html
<!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>

小结 ​

  1. querySelector() 返回第一个元素或 null。
  2. querySelectorAll() 返回静态 NodeList,可按索引访问和遍历。
  3. CSS 选择器可以直接复用于 DOM 查询。
  4. 范围查询可减少同名元素冲突。
  5. 父子、兄弟、closest() 与 matches() 能从当前元素继续查找。