先记住这个答案
:first-child 看的是元素在所有兄弟节点中的位置——只有排在第一个的才匹配。:first-of-type 看的是元素在同类型兄弟中的位置——只要是同标签里的第一个就匹配,前面有没有其他类型的元素不影响。这个区别在 HTML 结构里混合了多种标签时最容易暴露。
:first-child看所有兄弟的排行:first-of-type只看同类型兄弟- 前面插不同标签会破坏
:first-child - 两者优先级相同,都算一个伪类
混合标签时两者的选择差异
做个实验就清楚了。在一个 <div> 里先放一个 <h2>,再放一个 <p>。因为 <p> 不是父元素的第一个子节点,p:first-child 选不中它;但 <p> 是同类型兄弟里的第一个(前面没有其他 <p>),所以 p:first-of-type 能选中。
<h2> 在 <p> 前面HTML<style>
/* 选不中:第一个子节点是 h2,不是 p */
p:first-child { color: red; }
/* 能选中:p 是第一个 <p> 类型的兄弟 */
p:first-of-type { background: lightyellow; }
</style>
<div>
<h2>标题</h2>
<p>这段文字会有黄色背景,但不会变红</p>
<p>第二段</p>
</div>去掉 <h2>,让 <p> 成为第一个子节点,p:first-child 和 p:first-of-type 就同时命中了。所以两者在只有一种标签的结构里表现完全一样,区别只在混合标签时才显现。
实际开发中踩坑最多的场景
CMS 或后端模板经常在列表前面自动插入标题、广告位或隐藏的标记元素。比如你用 li:first-child 给列表第一项加圆角,结果运营在 <ul> 里第一个位置插了一个 <div> 广告,第一个 <li> 不再是 :first-child 了,圆角就丢了。
- 改用
:first-of-type更稳 - 这时换成
li:first-of-type就安全了,不管前面插了几个其他标签,第一个<li>始终能被选中。不过要注意,:first-of-type是按标签名判断的,不是按 class。同一个父元素下的.card:first-of-type实际上匹配的是第一个和.card同标签的元素,而不是第一个带.card类名的元素。 用 JavaScript 动态插入 DOM 节点时也一样。用insertBefore在列表开头插入新节点后,原来匹配:first-child的元素立刻失效,而:first-of-type只要新插入的不是同类型标签就不受影响。
容易答错的地方
- 以为
:first-of-type是按 class 筛选的 .card:first-of-type不是"第一个.card",而是"第一个与.card元素同标签的元素,并且它还得有.card类"。如果父元素下有两个<div>,第一个没有.card类而第二个有,:first-of-type命中的是第一个<div>,但它没有.card类,所以.card:first-of-type谁也选不中。- 混淆
:first-child和:nth-child(1) - 两者完全等价,
:first-child就是:nth-child(1)的简写。但:first-of-type和:nth-of-type(1)才是另一对等价写法,不要把:nth-child和:nth-of-type搞混了。
面试官还会怎么问?
怎么选中"第一个带某个 class 的元素"?
纯 CSS 没有"第一个匹配某 class 的元素"这种选择器。:first-of-type 是按标签名分组的,不看 class。一种变通方式是先给所有同 class 元素设默认样式,再用相邻兄弟选择器 .card ~ .card 把第二个及以后的覆盖掉,这样视觉上只有第一个保留特殊样式。
:first-child 在 <table> 里表现如何?
<table> 的直接子元素通常是 <thead> 和 <tbody> 等,不是 <tr>。写 tr:first-child 时,它匹配的是每个 <thead> 或 <tbody> 里的第一个 <tr>,不是整个表格的第一行。
用 :first-child 和 :first-of-type 写出来的选择器优先级一样吗?
一样。两者都是伪类,优先级都算 (0, 1, 0),和一个 class 选择器等权。要区分优先级高低还是得靠 id 选择器或者层叠顺序。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。