在写CSS选择器的时候,不少人习惯把每个伪类单独拎出来使用,比如给列表第一项加样式就用:first-child,隔行变色就用:nth-child(odd)。但实际项目中,一旦结构稍微复杂一点,比如列表里混入了标题、分割线,或者只想对特定范围的元素做特殊处理,单独一个伪类往往就不够用了。把:first-child和:nth-child组合起来,能够解决很多这种“定位模糊”的问题。下面通过几个具体结构来演示这种组合的写法和效果。

回顾两个伪类各自的工作方式
先说:first-child。它匹配的是父元素下的第一个子元素,而且这个子元素的标签类型必须和选择器前面写的元素一致。比如.item:first-child,只有当一个带有class="item"的元素恰好是它父元素的第一个子节点时,才会命中。注意这里说的是“第一个子节点”,不是“第一个带有该class的节点”。如果父元素的第一个子节点是别的标签,比如一个<span>或者一段文本节点,那这个:first-child就完全失效了。很多开发者遇到“明明列表第一项应该有样式但没生效”,原因多半就在这里。
再来看:nth-child。它接受一个参数,可以是数字、关键字odd/even,或者公式an+b。公式中的a和b都是整数,n从0开始依次递增。比如:nth-child(2n+1)表示第1、3、5……个子元素,:nth-child(-n+3)表示前三个子元素。和:first-child一样,它也是基于元素在父元素所有子节点中的位置来计算的,不管这个子节点是什么标签。因此,如果父元素里混入了文本节点或者其他标签,nth-child的计数照样会把这些节点算进去。理解了这两点,才能明白为什么组合起来能更精确地控制目标元素。
组合使用:先锁定首元素,再用nth-child补充筛选
最常见的组合场景,是针对一个列表中同时存在普通项和特殊项的情况。假设有一个导航菜单,HTML结构如下:
<ul class="menu"> <li class="item">首页</li> <li class="item">产品</li> <li class="item">服务</li> <li class="divider"></li> <li class="item">关于</li> <li class="item">联系</li> </ul>
现在需求是:给第一个带有class="item"的li设置特殊背景色,同时给除第一个之外的所有item设置统一样式。如果只用:first-child,第一个li确实能命中,因为它在ul里就是第一个子节点。但如果你试图用.item:not(:first-child)来给后续项设置样式,那第四个li(class="divider")虽然也是子节点,但它没有class="item",所以它不会被选中,真正需要处理的是第五个li。这时候把:first-child和:nth-child结合起来,可以写:
.menu li.item:first-child {
background-color: #f0f0f0;
font-weight: bold;
}
.menu li.item:nth-child(n+2) {
/* 因为第四个li是divider,不会被li.item匹配,
所以这里实际命中的是第2、3、5、6个li中的item */
padding-left: 20px;
}
上面的写法中,li.item:first-child精确锁定第一个子元素且类为item的li,而li.item:nth-child(n+2)表示从第二个子元素开始的所有li元素,再通过.item过滤掉divider,结果就是除了第一个item之外的所有item都应用了padding-left。有人可能会问,为什么不直接用.menu li:not(:first-child)?因为那样会把divider也算进去,而divider可能只是分割线,不需要padding。组合使用让选择器语义更明确。
处理父元素中混入非目标标签的情况
上面那个例子里的divider是li标签,实际项目中更常见的是父元素里混入了不同标签,比如一个卡片容器里有h3标题、p段落、还有多个div卡片项。如果要对“第一个卡片项”和“后面的卡片项”分别设置不同样式,直接写.card:first-child大概率不会生效,因为第一个子节点可能是h3标题。正确的做法是先确认目标元素在父元素中的真实位置,然后用nth-child配合类型选择器。
以一个典型的文章列表为例,HTML结构如下:
<div class="container"> <h3>推荐阅读</h3> <div class="card">卡片一</div> <div class="card">卡片二</div> <div class="card">卡片三</div> <div class="card">卡片四</div> </div>
在这种结构下,.card:first-child永远不会命中,因为container的第一个子节点是h3,而不是div.card。这时候可以用.card:nth-child(2)来选中第一个卡片,因为h3占据了第一个子节点位置。但这样写可读性差,而且一旦后续在h3前面又插入一个p标签,索引就得改。更稳健的方式是结合:first-of-type,但本文讨论的是:first-child和:nth-child的组合。实际上,可以用.card:nth-child(n+2):first-child这种看似矛盾的写法吗?不行,因为:first-child要求它是父元素的第一个子节点,而nth-child(n+2)要求它是第二个或之后的子节点,两者不可能同时满足。所以组合使用时要避免这种逻辑冲突。
正确的思路是:先用nth-child排除掉非目标标签,再用另一个伪类精准筛选。例如需要给所有.card应用统一样式,但第一个卡片(即父元素的第二个子节点)要加特殊边框,可以这样写:
.container .card:nth-child(2) {
border: 2px solid #333;
}
.container .card:nth-child(n+3) {
margin-top: 10px;
}
这里第一个卡片通过:nth-child(2)单独设定,后续卡片从第三个子节点开始设置上加边距。虽然没有直接组合使用:first-child,但体现了nth-child和结构位置配合的灵活性。如果你确实想在父元素有非目标首节点时也使用:first-child,就必须调整HTML结构,让目标元素真正成为第一个子节点,否则就只能依赖nth-child的具体索引。
组合选择器在表格和复杂列表中的实战应用
表格是练习nth-child的经典场景,但加上:first-child后可以做出更细腻的行样式。比如一个数据表格,第一行通常是表头(如果用thead就把表头隔离了,但有些表格用tr直接做表头),此时希望第一行文字加粗、背景深色,数据行从第二行开始隔行变色,并且每行的第一个单元格需要左对齐,其余居中。可以这样写:
/* 第一行作为表头 */
tr:first-child {
background-color: #2c3e50;
color: #fff;
font-weight: bold;
}
/* 从第二行开始的所有行 */
tr:nth-child(n+2) {
background-color: #ecf0f1;
}
/* 数据行隔行变色,覆盖上面背景 */
tr:nth-child(2n+3) {
background-color: #fafafa;
}
/* 每行第一个单元格左对齐 */
td:first-child {
text-align: left;
padding-left: 12px;
}
注意上面tr:nth-child(2n+3)实际对应第3、5、7……行,因为数据行从第2行开始,隔行变色从第3行开始交替。这样第一行表头被:first-child单独处理,后续行通过nth-child公式精确控制。组合起来虽然没有在同一选择器里同时写:first-child和:nth-child,但整体样式架构正是基于这两个伪类的分工合作。如果换成tr:nth-child(odd),会把第一行也算进去,导致表头样式被覆盖,不得不写更具体的覆盖规则。而用:first-child先锁定第一行,再用nth-child(n+2)排除掉第一行,逻辑更清晰。
在响应式卡片列表中,这种组合也很有用。比如一个网格布局,在移动端一列显示,桌面端三列显示,需要特别处理第一张卡片的上下间距,以及最后一行卡片的底部边距。可以这样写:
.card:first-child {
margin-top: 0;
}
.card:nth-child(3n+1) {
clear: left;
}
.card:nth-child(n+4) {
margin-top: 20px;
}
上面.card:first-child去掉了第一张卡片的上边距,:nth-child(3n+1)用来在每行第一列清除浮动(适用于float布局),:nth-child(n+4)给从第四张卡片开始添加顶部间距。实际项目中往往三个伪类规则同时作用于一张卡片,但它们的职责互不干扰。这种写法比单独依赖某一种伪类更加灵活,后续维护时也容易调整。
容易忽略的细节:计算规则与性能考量
在使用:first-child和:nth-child组合时,一个经常踩坑的细节是:两个伪类之间没有空格时,它们必须同时作用于同一个元素。比如li:first-child:nth-child(1)是正确的,它要求该li既是父元素的第一个子节点,又满足nth-child(1)的条件,两者等价。但li:first-child :nth-child(2)中间有空格,表示选中li:first-child内部的后代元素中所有满足:nth-child(2)的元素,这是后代选择器加上伪类的组合,完全不同。很多人会把空格写错导致样式应用范围变得不可控。
另一个细节是nth-child的公式计算是从1开始的,而不是从0。写:nth-child(2n)表示2、4、6……,写:nth-child(2n+1)表示1、3、5……。而:first-child本质上等价于:nth-child(1)。因此当你需要选择第一个元素时,用:first-child比用:nth-child(1)更直观。但如果你需要同时选择第一个和第二个元素,就不能用:first-child,得写:nth-child(-n+2)。组合使用时,如果写出:first-child:nth-child(n+2),由于n+2的最小值是2,永远不等于1,所以这个选择器不可能命中任何元素。这种“永远为空”的选择器写出来通常是逻辑错误,需要特别注意。
性能方面,CSS选择器的匹配是从右向左进行的。对于:first-child和:nth-child这类伪类,浏览器需要遍历该元素的父元素的子节点列表来计算位置。这种计算开销通常很小,但如果页面中有大量深层嵌套的元素,并且选择器写得很复杂,比如.container .item:first-child:nth-child(odd),浏览器在每个层级都要做位置判断。好在现代浏览器对伪类优化得很好,实际影响可以忽略,但在书写选择器时还是建议尽量短一些,比如能用类名区分就用类名,而不是完全依赖结构伪类。组合选择器的优势在于表达力强,而不是用来替代一切class方案。