:first-child要求目标元素必须是父元素的第一个子节点且标签名一致,若首个子节点为header、注释或文本节点则完全不匹配;而:first-of-type只找同类型元素中的第一个,无视其他节点类型。

为什么:first-child在父元素有非同类兄弟时完全不匹配
因为 :first-child 不是“找第一个 li”或“第一个 p”,而是硬性要求:目标元素必须同时满足两个条件——是父元素的 childNodes[0],且标签名一致。只要父元素第一个子节点是 <header></header>、<div class="tip">、注释 <code><!-- --> 或换行产生的 #text 节点,后续所有 li 或 p 都自动失去资格。
- 常见结构:
<ul> <header>标题</header><li>Item 1</li> </ul>→li:first-child永远无效,因为第一个子节点是header - 服务端渲染注入提示块、Vue/React 的
v-if分支、甚至 Web Components 的 shadow DOM 插入逻辑,都可能在开头悄悄塞进一个非li元素 - 调试时别信“源码看着是第一个”,直接在控制台执行
document.querySelector('ul').childNodes[0]看类型:nodeType === 1 是元素,3 是文本,8 是注释
:first-of-type 为什么在这种场景下更可靠
:first-of-type 完全不关心前面有什么,它只做一件事:遍历父元素所有子节点,把所有同名标签(比如全部 li)拎出来,按 DOM 顺序排好,取第一个。文本、注释、div、svg 全被跳过。
- 这个结构:
<ul> <!-- desc --><li>A</li> <li>B</li> </ul>→li:first-of-type匹配 A,li:first-child不匹配任何 - 新闻流中穿插广告:
<article></article><div class="ad"></div> <article></article>→article:first-of-type仍稳稳选中首个正文 - 注意:
:first-of-type不能替代对“绝对位置”的需求,比如你真要重置容器最顶部那个子元素的margin-top,那还是得靠:first-child+ 清理结构
动态插入后:first-child突然失效的典型原因
JS 插入新元素时,默认行为(appendChild()、innerHTML +=)不会让它成为第一个子节点;而用 prepend() 或 insertAdjacentElement('afterbegin', el) 强制置顶,也只在父元素开头“干净”时才真正生效。
-
el.innerHTML = '<li>New</li>' + el.innerHTML→ 重建整个子树,所有节点索引重算,原有:first-child状态清零 -
el.insertAdjacentHTML('afterbegin', '<li>New</li>')→ 如果开头已有注释或空格,新li仍是第二个子节点 - 更稳妥的做法:插入后手动加 class,比如
el.children[0].classList.add('is-first'),再写.is-first { ... }
什么时候非用:first-child不可
只有当你明确依赖“DOM 顺序上的第一个”,而不是“某类元素的第一个”时,才值得为 :first-child 投入结构治理成本。
- 需要统一重置容器首子元素的
margin-top,不管它是h1、img还是div:> *:first-child { margin-top: 0; } - 配合
:not()做精细过滤,比如li:first-child:not(.disabled),前提是 disabled 类一定不会出现在第一个位置 - Flex/Grid 布局中,视觉顺序被
order或grid-area打乱,但你还想锚定原始 DOM 第一个子项 —— 此时:first-child反而是唯一能守住“原始起点”的选择器
真正容易被忽略的是:哪怕你本地开发时结构“看起来干净”,上线后服务端模板引擎注入了隐藏 <div class="ssr-hint">,或者 CDN 自动插入了性能埋点脚本标签,<code>:first-child 就会静默失效——它不报错,也不警告,只是样式不出现。调试永远从 parentNode.childNodes 开始,而不是查优先级或拼写。











