Skip to content

DOM 选择器:精准定位页面元素的艺术

要修改网页上的任何内容,第一步就是找到它。在一个可能有成百上千个元素的页面中,精确地定位到你想要操作的那个元素,这就是 DOM 选择器的工作。

早期的 JavaScript 只提供了按 ID 和标签名查找元素的方法。随着 CSS 选择器的强大功能被广泛认可,现代浏览器也支持使用 CSS 选择器语法来查找 DOM 元素,让元素定位变得更加灵活和强大。

经典选择方法

这些方法从 DOM Level 1 时代就存在,在所有浏览器中都有良好的支持。

getElementById

通过元素的 id 属性获取单个元素。由于 HTML 规范要求 id 在文档中唯一,所以这个方法只返回一个元素(或 null)。

javascript
const header = document.getElementById("main-header");

if (header) {
  header.textContent = "Welcome to My Site";
}

几点需要注意:

  1. 参数是 ID 字符串,不需要加 # 前缀
  2. 如果找不到匹配的元素,返回 null
  3. 这是最快的元素查找方法,因为浏览器内部维护了 ID 到元素的映射
javascript
// ❌ 常见错误:加了 # 前缀
const wrong = document.getElementById("#header"); // 找不到

// ✅ 正确写法
const correct = document.getElementById("header");

getElementsByClassName

通过类名获取元素集合。返回一个实时的 HTMLCollection

javascript
const items = document.getElementsByClassName("menu-item");

console.log(items.length); // 匹配的元素数量

// 遍历所有匹配的元素
for (let i = 0; i < items.length; i++) {
  items[i].classList.add("active");
}

可以指定多个类名,用空格分隔:

javascript
// 查找同时拥有 btn 和 primary 两个类的元素
const primaryButtons = document.getElementsByClassName("btn primary");

getElementsByTagName

通过标签名获取元素集合:

javascript
const paragraphs = document.getElementsByTagName("p");
const allElements = document.getElementsByTagName("*"); // 获取所有元素

console.log(`页面上有 ${paragraphs.length} 个段落`);

getElementsByName

通过 name 属性获取元素,常用于表单元素:

javascript
// 获取所有 name="gender" 的单选按钮
const genderInputs = document.getElementsByName("gender");

// 找出被选中的选项
for (const input of genderInputs) {
  if (input.checked) {
    console.log("选中的值:", input.value);
  }
}

实时集合的特性

getElementsBy* 系列方法返回的都是实时集合(Live Collection)。这意味着当 DOM 发生变化时,集合内容会自动更新:

javascript
const divs = document.getElementsByTagName("div");
console.log(divs.length); // 假设是 5

// 创建并添加新的 div
const newDiv = document.createElement("div");
document.body.appendChild(newDiv);

console.log(divs.length); // 自动变成 6,无需重新查询

这个特性既是优点也是潜在的陷阱。在遍历并修改集合时要特别小心:

javascript
const items = document.getElementsByClassName("item");

// ❌ 危险:删除元素会导致索引变化,可能跳过元素或死循环
for (let i = 0; i < items.length; i++) {
  items[i].remove();
}

// ✅ 安全:从后向前遍历
for (let i = items.length - 1; i >= 0; i--) {
  items[i].remove();
}

// ✅ 安全:先转换为静态数组
[...items].forEach((item) => item.remove());

现代选择器方法

querySelectorquerySelectorAll 是现代 DOM API 的核心,它们使用 CSS 选择器语法来查找元素。

querySelector

返回匹配选择器的第一个元素:

javascript
// 按 ID 选择
const header = document.querySelector("#header");

// 按类名选择
const firstItem = document.querySelector(".item");

// 按标签选择
const firstParagraph = document.querySelector("p");

// 复杂选择器
const activeLink = document.querySelector("nav a.active");
const firstChild = document.querySelector("ul > li:first-child");

如果没有匹配的元素,返回 null。建议在使用前进行检查:

javascript
const element = document.querySelector(".maybe-exists");

if (element) {
  element.textContent = "Found!";
} else {
  console.log("Element not found");
}

// 或使用可选链
document.querySelector(".maybe-exists")?.classList.add("found");

querySelectorAll

返回所有匹配的元素,装在一个静态的 NodeList 中:

javascript
const allLinks = document.querySelectorAll("a");
const menuItems = document.querySelectorAll(".menu > li");
const highlighted = document.querySelectorAll(".highlight, .featured");

// 遍历结果
allLinks.forEach((link) => {
  console.log(link.href);
});

getElementsBy* 不同,querySelectorAll 返回的是静态快照。DOM 变化后,这个列表不会自动更新:

javascript
const divs = document.querySelectorAll("div");
console.log(divs.length); // 假设是 5

document.body.appendChild(document.createElement("div"));
console.log(divs.length); // 仍然是 5

// 需要重新查询才能获取最新结果
const freshDivs = document.querySelectorAll("div");
console.log(freshDivs.length); // 现在是 6

CSS 选择器语法速查

querySelectorquerySelectorAll 支持几乎所有的 CSS 选择器:

javascript
// 基础选择器
document.querySelector("#id"); // ID 选择器
document.querySelector(".class"); // 类选择器
document.querySelector("div"); // 标签选择器
document.querySelector("*"); // 通用选择器

// 组合选择器
document.querySelector("div.container"); // 同时满足
document.querySelector("div, p"); // 或(任一匹配)
document.querySelector("div p"); // 后代选择器
document.querySelector("div > p"); // 直接子代
document.querySelector("h1 + p"); // 紧邻兄弟
document.querySelector("h1 ~ p"); // 一般兄弟

// 属性选择器
document.querySelector("[disabled]"); // 有 disabled 属性
document.querySelector('[type="text"]'); // 属性等于
document.querySelector('[class^="btn-"]'); // 属性开头
document.querySelector('[href$=".pdf"]'); // 属性结尾
document.querySelector('[data-id*="user"]'); // 属性包含

// 伪类选择器
document.querySelector("li:first-child"); // 第一个子元素
document.querySelector("li:last-child"); // 最后一个子元素
document.querySelector("li:nth-child(2)"); // 第二个子元素
document.querySelector("li:nth-child(odd)"); // 奇数位子元素
document.querySelector("input:checked"); // 选中的复选框/单选框
document.querySelector("input:not([disabled])"); // 非禁用的输入框
document.querySelector("p:empty"); // 空元素
document.querySelector(":focus"); // 当前聚焦的元素

在元素上调用选择器

除了 document,选择器方法也可以在任何元素上调用,这样会限制搜索范围在该元素的后代中:

javascript
const form = document.querySelector("#user-form");

// 在 form 内部查找
const submit = form.querySelector('button[type="submit"]');
const inputs = form.querySelectorAll("input");
const email = form.querySelector('input[name="email"]');

这种方式比使用长的选择器字符串更高效,也更易于维护:

javascript
// ❌ 长选择器字符串
const email = document.querySelector('#user-form input[name="email"]');

// ✅ 分步查找,更清晰
const form = document.querySelector("#user-form");
const email = form.querySelector('input[name="email"]');

特殊元素的快捷访问

某些重要元素可以直接通过 document 的属性访问,无需选择器:

javascript
// 核心文档元素
document.documentElement; // <html> 元素
document.head; // <head> 元素
document.body; // <body> 元素

// 集合
document.forms; // 所有 <form> 元素
document.images; // 所有 <img> 元素
document.links; // 所有带 href 的 <a> 和 <area>
document.scripts; // 所有 <script> 元素
document.styleSheets; // 所有样式表

表单和表单元素还可以通过 name 属性快捷访问:

html
<form name="login">
  <input name="username" type="text" />
  <input name="password" type="password" />
</form>
javascript
// 通过 name 访问表单
const loginForm = document.forms["login"];
// 或
const loginForm = document.forms.login;

// 通过 name 访问表单元素
const username = loginForm.elements["username"];
const password = loginForm.elements.password;

closest 和 matches 方法

除了向下查找后代元素,有时还需要向上查找祖先元素或检查元素是否匹配选择器。

closest

从当前元素开始,向上查找(包括自身)第一个匹配选择器的祖先元素:

javascript
const deleteBtn = document.querySelector(".delete-btn");

// 向上查找包含它的 card 元素
const card = deleteBtn.closest(".card");

if (card) {
  card.remove();
}

closest 在事件委托场景中特别有用:

javascript
document.querySelector(".card-container").addEventListener("click", (e) => {
  // 检查点击的是否是删除按钮或其子元素
  const deleteBtn = e.target.closest(".delete-btn");

  if (deleteBtn) {
    const card = deleteBtn.closest(".card");
    card.remove();
  }
});

matches

检查元素是否匹配指定的选择器,返回布尔值:

javascript
const element = document.querySelector(".item");

console.log(element.matches(".item")); // true
console.log(element.matches(".active")); // 取决于元素是否有 active 类
console.log(element.matches("div.item")); // 取决于元素是否是 div

// 实际应用:根据元素类型执行不同操作
function handleClick(e) {
  if (e.target.matches("button")) {
    handleButtonClick(e.target);
  } else if (e.target.matches("a")) {
    handleLinkClick(e.target);
  } else if (e.target.matches("input")) {
    handleInputClick(e.target);
  }
}

选择器性能对比

不同的选择方法有不同的性能特征。虽然现代浏览器的性能已经很好,但在需要频繁查询或处理大量元素时,了解这些差异仍然有价值。

性能排序(从快到慢)

  1. getElementById - 最快,浏览器有直接映射
  2. getElementsByClassName - 很快,浏览器有优化
  3. getElementsByTagName - 很快
  4. querySelector - 稍慢,需要解析选择器
  5. querySelectorAll - 最慢,需要遍历匹配所有元素
javascript
// 性能敏感场景下的选择
// ✅ 优先使用 ID
const container = document.getElementById("container");

// ✅ 缓存频繁使用的元素
const header = document.getElementById("header");
// 而不是每次都调用 document.getElementById('header')

// ✅ 缩小搜索范围
const form = document.getElementById("form");
const inputs = form.querySelectorAll("input");
// 而不是 document.querySelectorAll('#form input')

实际应用建议

尽管 getElementById 最快,但性能差异在大多数情况下可以忽略不计。选择选择器时,更应该考虑代码的可读性和维护性:

javascript
// 在需要极致性能的场景使用 getElementById
const criticalElement = document.getElementById("critical");

// 在一般场景使用 querySelector,代码更简洁一致
const header = document.querySelector("#header");
const nav = document.querySelector(".main-nav");
const firstItem = document.querySelector(".list > li:first-child");

实际应用示例

表单验证

javascript
function validateForm(formId) {
  const form = document.getElementById(formId);
  const errors = [];

  // 查找所有必填字段
  const requiredFields = form.querySelectorAll("[required]");

  requiredFields.forEach((field) => {
    if (!field.value.trim()) {
      errors.push(`${field.name || field.id} 是必填项`);
      field.classList.add("error");
    } else {
      field.classList.remove("error");
    }
  });

  // 验证邮箱格式
  const emailField = form.querySelector('[type="email"]');
  if (emailField && emailField.value) {
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailPattern.test(emailField.value)) {
      errors.push("请输入有效的邮箱地址");
      emailField.classList.add("error");
    }
  }

  // 显示错误信息
  const errorContainer = form.querySelector(".error-messages");
  if (errorContainer) {
    errorContainer.innerHTML = errors.map((e) => `<p>${e}</p>`).join("");
  }

  return errors.length === 0;
}

动态内容高亮

javascript
function highlightSearchResults(keyword) {
  // 移除之前的高亮
  document.querySelectorAll(".highlight").forEach((el) => {
    const text = el.textContent;
    el.outerHTML = text;
  });

  if (!keyword) return;

  // 在所有段落中搜索并高亮
  const paragraphs = document.querySelectorAll("article p");
  const regex = new RegExp(`(${keyword})`, "gi");

  paragraphs.forEach((p) => {
    if (p.textContent.toLowerCase().includes(keyword.toLowerCase())) {
      p.innerHTML = p.innerHTML.replace(
        regex,
        '<span class="highlight">$1</span>'
      );
    }
  });

  // 滚动到第一个高亮位置
  const firstMatch = document.querySelector(".highlight");
  if (firstMatch) {
    firstMatch.scrollIntoView({ behavior: "smooth", block: "center" });
  }
}

表格排序

javascript
function sortTable(tableId, columnIndex) {
  const table = document.getElementById(tableId);
  const tbody = table.querySelector("tbody");
  const rows = [...tbody.querySelectorAll("tr")];

  // 判断当前排序方向
  const isAscending = table.dataset.sortDirection !== "asc";
  table.dataset.sortDirection = isAscending ? "asc" : "desc";

  // 排序
  rows.sort((a, b) => {
    const cellA = a.querySelectorAll("td")[columnIndex].textContent.trim();
    const cellB = b.querySelectorAll("td")[columnIndex].textContent.trim();

    // 尝试作为数字比较
    const numA = parseFloat(cellA);
    const numB = parseFloat(cellB);

    if (!isNaN(numA) && !isNaN(numB)) {
      return isAscending ? numA - numB : numB - numA;
    }

    // 作为字符串比较
    return isAscending
      ? cellA.localeCompare(cellB)
      : cellB.localeCompare(cellA);
  });

  // 更新表格
  rows.forEach((row) => tbody.appendChild(row));

  // 更新表头样式
  const headers = table.querySelectorAll("th");
  headers.forEach((th, index) => {
    th.classList.remove("sort-asc", "sort-desc");
    if (index === columnIndex) {
      th.classList.add(isAscending ? "sort-asc" : "sort-desc");
    }
  });
}

常见问题与解决方案

元素不存在时的处理

javascript
// ❌ 危险:如果元素不存在会报错
document.querySelector(".element").textContent = "Hello";

// ✅ 安全:先检查
const element = document.querySelector(".element");
if (element) {
  element.textContent = "Hello";
}

// ✅ 使用可选链(现代浏览器)
document.querySelector(".element")?.classList.add("active");

动态添加的元素

对于页面加载后动态添加的元素,需要在添加之后再查询,或者使用事件委托:

javascript
// 场景:通过 AJAX 加载并插入内容后
fetch("/api/content")
  .then((res) => res.text())
  .then((html) => {
    document.querySelector("#container").innerHTML = html;

    // 现在才能选择新添加的元素
    const newItems = document.querySelectorAll(".new-item");
    newItems.forEach((item) => {
      item.addEventListener("click", handleClick);
    });
  });

// 更好的方案:事件委托
document.querySelector("#container").addEventListener("click", (e) => {
  if (e.target.matches(".new-item")) {
    handleClick(e);
  }
});

选择器语法错误

javascript
// ❌ 无效选择器会抛出异常
try {
  document.querySelector("[invalid syntax");
} catch (e) {
  console.error("选择器语法错误:", e.message);
}

// ✅ 对于动态构建的选择器,使用 try-catch
function safeQuery(selector) {
  try {
    return document.querySelector(selector);
  } catch (e) {
    console.error("无效的选择器:", selector);
    return null;
  }
}

总结

DOM 选择器是操作网页的第一步。掌握不同选择方法的特点,能让你在不同场景下做出最佳选择:

方法返回值实时性适用场景
getElementById单个元素-已知 ID 的唯一元素
getElementsByClassNameHTMLCollection实时按类名批量获取
getElementsByTagNameHTMLCollection实时按标签批量获取
querySelector单个元素-复杂选择器取第一个
querySelectorAllNodeList静态复杂选择器取全部
closest单个元素-向上查找祖先
matches布尔值-检查是否匹配

在日常开发中,querySelectorquerySelectorAll 因其灵活性和一致性成为首选。但在性能敏感的场景,仍然值得使用 getElementById 这样的专用方法。