 伪类深度解析:从基础语法到实战技巧)
先说我最近重写的一个页面需求一长串商品卡片要求每张卡片的左上角露出一个“第几件”的角标。后端返回的是数组前端循环渲染数字直接写死在模板里结果产品说要支持手动排序排序一变数字全乱了。后来我把角标里的数字全部删掉改成由 CSS 的:nth-child()配合计数器来自动显示序号结构、样式、逻辑彻底分开这个需求再也没让人返工过。没错今天想聊的就是 CSS 里头这个非常基础、却经常用不明白的选择器——:nth-child()。可能有人觉得一个伪类有什么好写的直接用就行。但我在带新人和做技术评审的时候发现十个前端里至少有五六个分不清:nth-child()和:nth-of-type()还有一批人被AnB表达式绕晕碰到“前三个”“每三个一组取第一个”这类需求只能去写 JS 循环。这篇文章把:nth-child()的完整用法从数字、关键词到AnB公式从基础匹配到和:nth-of-type()的对比再加上几个能直接抄走的实战场景一次讲清楚。刚入门的前端可以当系统教程读写过几年的老手也可以当速查手册查漏补缺平时写样式时想不起来某个写法翻到对应章节看一眼就行。1. 先把 :nth-child() 选中的逻辑说清楚1.1 它解决的是“按位置给样式”这类问题平时写页面最常见的是给 class 加样式classactive就变红classdisable就变灰。但如果需求是“列表里第 2 个、第 4 个、第 6 个卡片左边加一条装饰线”你不可能在 HTML 里逐个写 class更不可能在接口返回的数据里塞 index 字段。位置相关的样式就应该交给位置相关的选择器。:nth-child()做的正是这件事它以父元素为参照物数这个父元素下所有直接子元素的序号然后选中第 N 个。这里有个关键限定词——直接子元素意思是 HTML 结构上上一级是同一个父节点的那些元素也就是常说的兄弟节点。它不看标签名、不看 class、不看 id只看位置序号这也是它在处理动态内容时特别稳的原因数据怎么变只要结构顺序不变样式就能固定落在预期位置。1.2 从 1 开始计数以及它的取值范围CSS 里:nth-child()的下标从 1 开始:nth-child(1)选中的是第一个孩子。这一点和 JavaScript 数组从 0 开始完全是两个维度很多人第一次写:nth-child(0)发现什么都没选中就是因为这个下标概念没转换过来。另外AnB公式里的 n 是从 0 开始取值的自然数n 依次取 0、1、2、3……一直往上计算出来的位置小于等于 0 的结果会被直接忽略。理解这两个“从几开始”后面所有公式都不会再算错。跑个简单例子:nth-child(1n0)和:nth-child(n)等价n0 时算出位置 0无效n1 时算出位置 1n2 时算出位置 2以此类推选中的就是全部子元素。这两个概念分开记就不容易乱了。1.3 三种参数形态总览先把:nth-child()的三种参数形态列出来后面对照着看会很清楚形态例子适用场景数字:nth-child(3)选中唯一的第 3 个元素关键词:nth-child(odd)/:nth-child(even)奇偶行最常见的是隔行变色AnB:nth-child(2n1)自定义间隔比如每 3 个取第 1 个数字形态最好理解但实际使用频率并不高因为写死的序号意味着结构一旦增删就会错位。关键词形态里odd等于2n1even等于2n。AnB是完整形态也是需要重点掌握的。许多人看到2n1这种表达式会懵其实把它看成“每隔 A 个元素取 1 个从第 B 个位置开始”就行2n1就是每 2 个取第 1 个等价于奇数位3n1就是每 3 个取第 1 个-n3则是倒着取最后落到第 1、2、3 个。这个理解方式比我见过的很多教程都直观先记住这个思路再看具体计算就轻松了。2. AnB 公式的拆解与常见写法2.1 手把手推一遍公式公式光说理论不够直观我用一个父元素下有 6 个 div 的例子来推。假设 HTML 结构是这样div classlist divA/div divB/div divC/div divD/div divE/div divF/div /div选择器.list div:nth-child(2n1)。n 从 0 开始取值n0 时位置是 1n1 时位置是 3n2 时位置是 5n3 时位置是 7但列表只有 6 个元素位置 7 不存在所以最终选中的是 A、C、E。想选中偶数位就用2n命中 B、D、F。想选中前 3 个用-n3n0 得到位置 3n1 得到位置 2n2 得到位置 1n3 得到位置 0无效命中的就是 A、B、C。想选中从第 4 个开始的所有元素用n4n0 得到位置 4n1 得到位置 5n2 得到位置 6继续往上直到超出集合范围命中的是 D、E、F。这几个例子覆盖了最常见的三种需求间隔取、取前几个、从第几个开始。2.2 想选第 3 到第 5 个用两个 :nth-child() 取交集如果要把范围限定在“第 3 到第 5 个”这一段单个公式搞不定需要两个:nth-child()条件叠加写成:nth-child(n3):nth-child(-n5)。这句话是交集的意思拆开看:nth-child(n3)匹配位置大于等于 3 的元素也就是 C、D、E、F:nth-child(-n5)匹配位置小于等于 5 的元素也就是 A、B、C、D、E两个集合相交正好是 C、D、E即第 3 到第 5 个。这种链式叠加的技巧在绘制表格区间、图表标注这类精细样式的时候特别有用也是面试里常问的一个进阶考点。记住一个原则上限用负 A 的公式下限用正 A 的公式方向别搞反。2.3 关键词 odd 和 even 的一个细节odd和even从名字上就很好懂但别忽略一个细节奇偶是相对父元素下全部直接子元素而言的不是相对某个 class 的集合。如果父元素里有 img、span 这种非列表标签它们同样参与奇偶计数。举个例子父容器里有 5 个 div 和 1 个 imgimg 恰好排在第 4 位那么div:nth-child(even)选中的是位置为 2、4、6 的三个 div而不是 div 集合里的偶数位。这个细节七八成的人都踩过坑尤其是页面里混了图标字体、装饰性 span 的时候隔行变色突然错位怎么查都找不到原因。所以用之前先看一眼父节点下面到底有哪些标签别光看目标元素本身。2.4 常用参数速查表为了方便查阅我把常见需求对应的写法整理成一张表实战里大多数写法都是这张表的变体需求写法第 1 个:nth-child(1)最后 1 个:nth-last-child(1)偶数位:nth-child(even)或:nth-child(2n)奇数位:nth-child(odd)或:nth-child(2n1)前 3 个:nth-child(-n3)从第 4 个开始:nth-child(n4)第 3 到第 5 个:nth-child(n3):nth-child(-n5)每 3 个取第 1 个:nth-child(3n1)每 3 个取第 3 个:nth-child(3n)从第 2 个开始每隔 2 个:nth-child(2n2)表里值得特别注意的是:nth-last-child()它从后往前数这样“最后一行”“最后三个”这类需求就不用先去数总数了。:nth-last-child(-n3)选中的是倒数第 1、2、3 个这种写法在表格底部合计行、列表末尾操作按钮等场景里非常常用。建议把这张表收藏起来写样式时直接用比现场推导公式快得多。3. 最容易踩坑的 :nth-of-type()以及它和 :nth-child() 的区别3.1 两者计数方式的本质差异:nth-child()和:nth-of-type()的差别是高频考点也是实际写样式最容易出错的地方。直接看一个例子。假设结构是这样div classdemo h2标题/h2 p第一段/p p第二段/p span标注/span /divp:nth-child(2)能选中“第一段”因为 p 恰好是父元素下的第 2 个子节点。但如果结构变成这样div classdemo h2标题/h2 span标注/span p第一段/p p第二段/p /divp:nth-child(2)就什么都选不中了因为第 2 个子节点是 span。而p:nth-of-type(2)在两种结构里都能选中“第二段”它数的是所有 p 标签里的第二个中间混了 h2、span 都不影响。一句话概括:nth-child()对所有标签统一编号:nth-of-type()对同类标签分组编号。这个区别记牢很多玄学样式问题能直接破案。3.2 组合写法更严格的位置限定如果既要限定“父元素的第 2 个孩子”又要求这个孩子必须是 p可以写成p:nth-child(2)它在两种结构里的行为和前面分析的完全一致。同样地:nth-of-type()也能和其他选择器组合比如.highlight:nth-of-type(3)表示“某个类名下第三种标签的第 3 个”这种写法在富文本排版、动态卡片流里偶尔会用到。另外还有一种常见的组合是section:nth-child(1) p:first-child用结构性伪类加后代选择器来定位复杂的页面区域。组合写法不用背太多关键是搞清楚每个选择器数的是什么集合集合概念对了组合只是排列问题。3.3 实际项目里怎么选我的经验是分几种情况来定如果列表元素全部是同一个标签比如 ul 里的 li直接用li:nth-child()就够语义清楚性能也好如果同一个父级下混着多种标签又只想选中其中某一种用:nth-of-type()如果需求是严格的“父元素的第 N 个子节点”那个子节点恰好是什么标签都无所谓用:nth-child()如果是“所有同类标签中的第 N 个”用:nth-of-type()。还有一个小众场景比如父容器里只有一个 div 和很多 span想选中那个唯一的 div用div:nth-of-type(1)比div:nth-child(1)更不容易受干扰。记住这套选择逻辑面试被问到也能答得利落。4. 实战场景几个可以直接抄走的写法4.1 表格隔行变色与局部高亮表格是:nth-child()最经典的落地场景。需求偶数行浅灰背景前两行加粗最后一行的底部分隔线加粗。HTML 结构先写好table classdata-table trtd行1/td/tr trtd行2/td/tr trtd行3/td/tr trtd行4/td/tr /table对应样式直接写.data-table tr:nth-child(even) { background: #f8f9fa; } .data-table tr:nth-child(-n2) { font-weight: 600; } .data-table tr:last-child { border-bottom: 2px solid #333; }这里要特别提醒一个坑如果表格用了 thead表头里的 tr 也会占据一个子元素序号tbody 里的 tr 编号会整体顺延隔行变色的黄金分割点很可能错位。我之前帮同事排查过一个类似问题表格第一行永远是白色后来发现表头占掉了:nth-child(1)的位置tbody 第一行其实是第 2 个孩子:nth-child(even)自然从 tbody 第二行才开始变灰。解决办法是表头单独用thead th写样式tbody 里的行改用:nth-of-type()或者干脆把表头视觉上放 tbody但语义上不推荐。4.2 网格卡片换行与间隙修正网格卡片布局里:nth-child()最常见的用途是修正每行首尾卡片的间距。假设一行四列卡片用 flex 布局每张卡片有统一的 margin那第一张卡片的左 margin 和第四张卡片的右 margin 就会造成视觉偏移。传统做法是给第一张和第四张加 class但动态列表没法这么干。正确姿势是用4n1和4n.card { margin: 12px; width: calc((100% - 96px) / 4); } .card:nth-child(4n1) { margin-left: 0; } .card:nth-child(4n) { margin-right: 0; }4n1匹配位置 1、5、9……也就是每行的第一张4n匹配位置 4、8、12……每行的最后一张。这套写法在老项目里几乎是唯一干净的解法因为没条件把 flex 全部改成 grid 的 gap。注意如果最后一行的卡片不满 4 张:nth-child(4n)仍然只选中第 4、第 8 张不会对残缺行造成额外影响这点可以放心。4.3 动态列表的序号角标这个玩法配合 CSS 计数器能实现“自动给动态列表编号”。比如一个排队叫号列表ul classqueue-list li任务一/li li任务二/li li任务三/li li任务四/li /ul.queue-list { counter-reset: queue-counter; } .queue-list li { counter-increment: queue-counter; } .queue-list li::before { content: counter(queue-counter) .; }这条思路本身不直接依赖:nth-child()但如果你要的是“序号从第 4 项开始展示”就可以结合:nth-child(n4)来限定范围.queue-list li:nth-child(n4)::before { content: counter(queue-counter) .; }这样前三条不显示序号从第四条开始自动编号。我在一个排队叫号面板里用过这套方案后端数据不断轮询刷新前端不需要为序号写任何 JS 逻辑纯 CSS 自动维护省心很多。原理就是:nth-child()只认结构位置数据怎么变都不影响。4.4 多列列表的分隔线处理在图文混排或者长列表里经常要“每个条目之间加一条虚线最后一项没有”。最简单的是相邻兄弟选择器.item .item { border-top: 1px dashed #ddd; }但如果需求更复杂比如“每三个一组组内之间用实线组之间用虚线”:nth-child()的优势就出来了.item:nth-child(3n):not(:last-child) { border-bottom: 2px solid #333; } .item:nth-child(3n1):not(:first-child) { border-top: 2px dashed #ccc; }:nth-child(3n)选中每组第 3 个也就是组的最后一项给它加粗实线当作分组分隔:nth-child(3n1)选中每组第 1 个从第二个组开始加一条虚线作组间分隔。:not(:last-child)和:not(:first-child)是防止第一条多出来多余边框。这种组合选择器写起来很爽但记得在代码注释里写清楚逻辑否则过两个月自己都看不懂这串东西在干嘛。4.5 响应式断点下的列表重排:nth-child()经常和媒体查询一起用。比如 PC 上是 4 列平板变成 2 列手机变成 1 列。列数一变每行第一张和最后一张的定位也要跟着变.card:nth-child(4n1) { margin-left: 0; } media (max-width: 768px) { .card { width: calc((100% - 48px) / 2); } .card:nth-child(2n1) { margin-left: 0; } .card:nth-child(2n) { margin-right: 0; } }很多新手在断点切换后发现样式乱掉往往是因为只改了容器宽度忘了同步修改:nth-child()的步长和偏移。我习惯在写这类样式时把“当前断点下每行几个卡片”直接写成注释比如“平板 2 列步长 2偏移 0”排查起来快很多。这个习惯一开始觉得多余后来在维护旧项目时帮我省了至少一个下午的调试时间。5. 常见问题与排查技巧实录5.1 我踩过的五个坑第一个坑把:nth-child(0)当第 0 个用。CSS 里没有第 0 个元素写 0 结果永远是空选择器。第二个坑把:nth-child(2n1)当成某个 class 集合里的奇数位忽略了中间夹杂的其他标签也参与计数。举一个真实例子一个富文本编辑器输出的文章列表h2、p、img 混排用户要求第一张图片加边框新手写了img:nth-child(1)结果怎么都选不中。正确做法是用img:nth-of-type(1)或者把正文区域按段落块拆分让每张图片都包裹在独立的容器里。第三个坑在嵌套结构里忘了:not(:last-child)。比如li:last-child用border-bottom: none清理底部边框但 li 内部又嵌套了 ul外层的 li 和里层的 li 都会参与计数越嵌套越乱。写:nth-child()前先想清楚这个选择器作用在哪个层级上千万别隔层套。第四个坑把两个:nth-child()条件的方向写反。比如第 3 到第 5 个写成了:nth-child(n3):nth-child(n5)这等于位置大于等于 3 且位置大于等于 5实际选中的是第 5 个及以后。正确写法是n3配合-n5取交集上限用负 A下限用正 A这个口诀记一下基本不会错。第五个坑动态渲染列表时无端失效。这个不一定是:nth-child()本身的问题常见原因是接口数据里存在空节点或条件渲染的占位符造成 DOM 结构顺序与视觉顺序不一致。遇到这种问题先数一遍实际渲染出来的 DOM 节点而不是盯着一开始预想的 HTML 结构猜。5.2 一条可靠的排查路径遇到:nth-child()相关样式不定我一般按下面顺序排查打开 DevTools在 Elements 面板里展开父元素数一遍直接子节点确认目标元素的位置序号。用 console 里的document.querySelectorAll()快速验证选择器到底命中了哪些元素。把选择器逐步拆成单个条件比如先试:nth-child(2)再试:nth-child(2n)定位是哪一层匹配出了问题。检查是否有同级选择器改写了相同属性导致样式优先级覆盖。这种情况和:nth-child()本身无关但调试时的表象几乎一模一样。这套路径帮我解决过不少结构复杂页面的样式问题比在样式表里瞎猜高效得多。5.3 关于性能和兼容性的一句话顺带提一嘴性能。:nth-child()在绝大多数现代浏览器里的匹配速度非常快即使一个父元素下有几百个节点也完全不用担心。只有在极端情况下比如同时操作几万个节点的列表且样式频繁重算:nth-child()带来的样式计算才会显示明显开销那时候优先考虑减少节点数量和把可变样式搬到 CSS 变量上。日常业务代码里基本不用为这个选择器做性能优化。兼容性方面:nth-child()从 IE9 开始就支持 AnB 表达式市面上还在用的环境基本都能跑不需要加前缀也不需要 polyfill。最后分享一个我在实际项目里的习惯每次用到:nth-child()这类结构性伪类我都会在写完样式后顺手在代码注释里写清楚步长、偏移和当前列数。看起来很小的一件事但六个月后改需求的人通常就是我自己能省下不少确认结构的时间。如果这篇文章里的某张速查表或者某个实战写法帮到你了建议直接收藏等踩坑的时候回来看一眼就好。