跟着 MDN 学 HTML day_28:(使用选择器 API 在 DOM 树中进行选择与遍历)

引言

在早期的前端开发中,要通过 JavaScript 获取页面中的特定元素,往往需要编写循环遍历的代码,或者依赖 getElementById、getElementsByClassName 等方法进行多次调用。随着 Selectors API 的引入,开发者可以直接使用 CSS 选择器语法来查询 DOM 树中的元素,这使得元素查找变得更加高效和直观。本文将详细介绍 querySelector 和 querySelectorAll 这两个核心方法,以及使用选择器时需要注意的细节。

一、NodeSelector 接口与两个核心方法

Selectors API 为所有实现了 Document、DocumentFragment 和 Element 接口的对象添加了两个新方法。这意味着不仅可以在整个文档中进行查询,还可以在特定的文档片段或元素子树中进行局部查询。

html 复制代码
<div id="container">
  <p class="intro">第一段介绍文字</p>
  <p class="content">第二段正文内容</p>
  <span class="content">这是一个 span 元素</span>
</div>

<script>
  // querySelector 返回第一个匹配的元素
  const firstContent = document.querySelector('.content');
  console.log(firstContent.textContent); // "第二段正文内容"(p 元素在前)

  // 如果没有匹配的元素,返回 null
  const notFound = document.querySelector('.nonexistent');
  console.log(notFound); // null

  // querySelectorAll 返回所有匹配的元素
  const allContent = document.querySelectorAll('.content');
  console.log(allContent.length); // 2(p 和 span)
  allContent.forEach(el => {
    console.log(el.tagName, el.textContent);
  });
  // P 第二段正文内容
  // SPAN 这是一个 span 元素

  // 如果没有匹配的元素,返回空的 NodeList
  const emptyResult = document.querySelectorAll('.nonexistent');
  console.log(emptyResult.length); // 0
</script>

querySelector 始终返回第一个匹配的元素,如果查找目标是唯一元素(如通过 ID 查询),这个方法是最高效的选择。querySelectorAll 则返回所有匹配元素的集合,适合批量操作场景。

二、在特定子树中进行查询

查询方法可以基于任意元素节点调用,而不仅仅局限于 document 对象。这样可以将搜索范围限制在特定的 DOM 子树中,提高查询效率并避免意外匹配到范围外的元素。

html 复制代码
<article id="post-1">
  <h2 class="title">文章一的标题</h2>
  <p class="summary">文章一的摘要</p>
</article>

<article id="post-2">
  <h2 class="title">文章二的标题</h2>
  <p class="summary">文章二的摘要</p>
</article>

<script>
  const post1 = document.getElementById('post-1');
  const post2 = document.getElementById('post-2');

  // 在 post-1 的子树中查询,不会匹配到 post-2 的元素
  const titleInPost1 = post1.querySelector('.title');
  console.log(titleInPost1.textContent); // "文章一的标题"

  const summaryInPost2 = post2.querySelector('.summary');
  console.log(summaryInPost2.textContent); // "文章二的摘要"

  // 子树查询不会包含调用节点本身,只搜索其子节点
  // 下面的查询在 post-1 内部查找 article 元素
  const articleInside = post1.querySelector('article');
  console.log(articleInside); // null(post-1 本身不会被匹配)
</script>

在复杂页面中,限定查询范围可以显著减少浏览器需要遍历的节点数量。当处理动态加载的内容或组件时,在组件根元素上调用查询方法是一种良好的实践。

三、querySelectorAll 返回的是静态 NodeList

与其他 DOM 方法(如 getElementsByClassName)返回的动态 HTMLCollection 不同,querySelectorAll 返回的是一个静态的 NodeList。这意味着获取结果后,后续对 DOM 的修改不会自动反映到这个列表中。

html 复制代码
<ul id="task-list">
  <li class="task">任务一</li>
  <li class="task">任务二</li>
</ul>
<button id="add-task">添加任务</button>
<button id="check-static">检查静态列表</button>

<script>
  const taskList = document.getElementById('task-list');
  const addBtn = document.getElementById('add-task');
  const checkBtn = document.getElementById('check-static');

  // 通过 querySelectorAll 获取静态 NodeList
  const staticTasks = document.querySelectorAll('.task');
  console.log('初始数量:', staticTasks.length); // 2

  // 通过 getElementsByClassName 获取动态 HTMLCollection
  const liveTasks = document.getElementsByClassName('task');
  console.log('动态集合初始数量:', liveTasks.length); // 2

  addBtn.addEventListener('click', () => {
    const newTask = document.createElement('li');
    newTask.className = 'task';
    newTask.textContent = `任务${taskList.children.length + 1}`;
    taskList.appendChild(newTask);

    console.log('添加后动态集合数量:', liveTasks.length); // 3(自动更新)
  });

  checkBtn.addEventListener('click', () => {
    console.log('静态列表数量:', staticTasks.length); // 始终为 2
    // 需要重新查询才能获取最新的元素
    const refreshed = document.querySelectorAll('.task');
    console.log('重新查询后数量:', refreshed.length);
  });
</script>

静态特性有时是优点:如果需要在遍历 NodeList 的同时修改 DOM 结构,静态列表可以避免因集合动态变化而导致的遍历问题。但如果需要始终反映最新的 DOM 状态,则需要重新执行查询。

四、灵活使用选择器列表

两个查询方法都支持选择器列表,可以在一次调用中使用多个逗号分隔的选择器,匹配其中任意一个的元素都会被返回。

html 复制代码
<div id="dashboard">
  <button class="primary">主要操作</button>
  <button class="secondary">次要操作</button>
  <a class="primary" href="/more">了解更多</a>
  <span class="secondary">提示信息</span>
</div>

<script>
  // 通过逗号分隔的选择器列表,一次性匹配多种元素
  const clickableElements = document.querySelectorAll(
    'button.primary, a.primary, button.secondary'
  );
  console.log('可交互元素数量:', clickableElements.length); // 3

  clickableElements.forEach(el => {
    console.log(el.tagName, el.className);
  });
  // BUTTON primary
  // BUTTON secondary
  // A primary

  // 选择器列表在 querySelector 中同样适用
  // 返回按文档顺序第一个匹配的元素
  const firstInteractive = document.querySelector(
    'span.secondary, a.primary, button.primary'
  );
  console.log(firstInteractive.tagName, firstInteractive.className);
  // BUTTON primary(按照文档顺序,button.primary 先出现)
</script>

选择器列表中的每个选择器都是独立匹配的,最终的排列顺序由元素在文档中的先后位置决定,而非选择器在列表中的书写顺序。这一特性在需要同时关注多种类型元素时非常实用。

五、通过 ID 进行查询

使用选择器 API 可以通过 ID 选择器来查找元素,甚至可以一次传入多个 ID 选择器,返回第一个存在于文档中的匹配元素。

html 复制代码
<div id="section-a">这是 A 区域</div>
<!-- 注意:文档中没有 id="section-b" 的元素 -->
<div id="section-c">这是 C 区域</div>

<script>
  // 单个 ID 查询
  const sectionA = document.querySelector('#section-a');
  console.log(sectionA.textContent); // "这是 A 区域"

  // 使用多个 ID 选择器:返回第一个在文档中存在的元素
  const firstSection = document.querySelector('#section-b, #section-c, #section-a');
  console.log(firstSection.textContent);
  // "这是 C 区域"(section-c 在文档顺序中先于 section-a 出现且存在)

  // 如果所有 ID 都不存在,返回 null
  const missing = document.querySelector('#nonexistent-1, #nonexistent-2');
  console.log(missing); // null

  // 相比 getElementById 的优势:可以一次尝试多个 ID
  // 等效的传统写法需要多次调用和条件判断
  const el = document.getElementById('section-b')
          || document.getElementById('section-c')
          || document.getElementById('section-a');
  console.log(el.textContent); // "这是 C 区域"
</script>

使用多 ID 选择器可以简化一些防御性编程场景:当不确定页面上是否存在某个 ID 时,可以按优先级列出备选 ID,浏览器会返回第一个存在的元素。这比手动编写多个 getElementById 调用的回退逻辑更加简洁。

六、伪类选择器的限制

出于隐私保护和安全性考虑,选择器 API 对某些伪类选择器的支持有限制。特别是 :visited 伪类不会返回任何匹配结果,而 :link 会被当作 :any-link 处理。

html 复制代码
<nav>
  <a href="https://example.com" class="external">外部链接(未访问)</a>
  <a href="https://developer.mozilla.org" class="external">MDN 链接</a>
  <a href="/local" class="local">内部链接</a>
</nav>

<script>
  // :link 伪类会被当作 :any-link 处理
  const allLinks = document.querySelectorAll('a:link');
  console.log('匹配到的链接数量:', allLinks.length);
  // 可能匹配所有带有 href 属性的 a 元素

  // :visited 伪类不会返回任何匹配结果
  const visitedLinks = document.querySelectorAll('a:visited');
  console.log('visited 匹配数量:', visitedLinks.length); // 0(始终为空)

  // 这种限制防止了恶意网站通过检测链接样式来窥探用户浏览历史
  // 在开发中应避免依赖 :visited 进行元素选择

  // 可以正常使用的结构伪类
  const firstLink = document.querySelector('a:first-of-type');
  console.log(firstLink.textContent); // "外部链接(未访问)"

  const noHrefLinks = document.querySelectorAll('a:not([href])');
  console.log('没有 href 的链接数量:', noHrefLinks.length);
&lt;/script&gt;

了解这些限制可以帮助开发者避免在元素查询中编写无效的选择器。如果需要根据链接状态来改变行为,应该在 CSS 层面处理样式,在 JavaScript 层面通过其他数据属性来标记状态。

总结

Selectors API 是现代 Web 开发中进行 DOM 查询的基石,它让元素查找变得像编写 CSS 一样自然。本文涵盖的核心要点包括:

querySelector 返回子树中第一个匹配的元素,未找到时返回 null。querySelectorAll 返回所有匹配元素的静态 NodeList,未找到时返回空列表。这两个方法可以在 Document、DocumentFragment 和 Element 上调用,支持在特定子树中限定查询范围。返回的 NodeList 是静态的,DOM 的后续变化不会自动反映到已获取的集合中。选择器列表允许在一次调用中组合多个选择器,匹配顺序由文档结构而非选择器书写顺序决定。多 ID 选择器可以简化需要回退查找的场景。出于隐私保护,:visited 伪类始终返回空结果,:link 会被当作 :any-link 处理。

掌握这些知识后,开发者可以告别冗长的循环遍历代码,用简洁高效的选择器语法精确地定位到目标元素。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
东北甜妹1 小时前
K8s Ingress
java·运维·前端
RickyWasYoung1 小时前
【Matlab】合并多个子图的fig文件为一个大图
前端·matlab·信息可视化
爱滑雪的码农1 小时前
React+three.js之项目搭建
前端·javascript·react.js
张风捷特烈1 小时前
状态管理大乱斗#07 | Signals 源码评析 - 暗流涌动
android·前端·flutter
沉浸式学习ing1 小时前
音视频内容怎么快速消化?视频转思维导图+精华速览的方法
人工智能·学习·ai·音视频·知识图谱·xmind
接着奏乐接着舞3 小时前
sse 两种调用方式
前端·javascript·vue.js
不会敲代码19 小时前
手写 Mini React:从 JSX 到虚拟 DOM 再到 render,搞懂 React 底层原理
前端·javascript·react.js
lichenyang45310 小时前
媒体选择、上传与音频采集 API 实现流程
oracle·音视频·媒体·android-studio
kyriewen10 小时前
你的代码仓库变成“毛线团”了?Monorepo 用 Turborepo 拆成“乐高积木”
前端·javascript·面试