文章

CSS伪类与伪元素深入解析

伪类与伪元素的区别、:nth-child vs :nth-of-type 高频陷阱、::before/::after 的 content 与实战用法,面试常考。

CSS伪类与伪元素深入解析

一句话概括

伪类和伪元素是 CSS 选择器的两大扩展:伪类(单冒号 :)描述元素的”状态或位置”(如 :hover、:nth-child),伪元素(双冒号 ::)凭空造出一个”虚拟节点/片段”(如 ::before、::first-line)。两者都不用改 HTML,却能精准命中各种”基于状态或结构”的样式需求。

一句话记住:伪类不产生新盒子,只是给已有元素贴标签;伪元素会产生一个新盒子,受 display/position 控制。这是面试分辨两者的本质标准。

核心知识点

1. 伪类(:)状态/位置,伪元素(::)虚拟节点

1
2
3
4
5
6
7
8
/* 伪类:描述状态或位置,不增加元素 */
a:hover { color: red; }              /* 鼠标悬停状态 */
input:focus { outline: 2px solid; }  /* 聚焦状态 */
li:first-child { font-weight: bold; }/* 位置:第一个子元素 */

/* 伪元素:凭空造节点,要写 content */
.p::before  { content: "→ "; color: #42b883; }
p::first-line { font-weight: bold; } /* 选中"第一行"片段 */

CSS3 规范建议伪类用单冒号、伪元素用双冒号(::before),但浏览器为兼容也接受单冒号写法。

2. :nth-child vs :nth-of-type 高频陷阱

这是面试最高频的陷阱题,关键在”数的是谁”:

1
2
3
4
5
6
<div class="box">
  <p>段落1</p>
  <span>内联1</span>
  <p>段落2</p>
  <span>内联2</span>
</div>
1
2
3
.box :nth-child(2)   { color: red; }   /* 第 2 个子元素 → 内联1 变红 */
.box p:nth-child(2)  { color: blue; }  /* 第 2 个子元素且是 p → 没有,选不到 */
.box p:nth-of-type(2){ color: blue; }  /* p 里的第 2 个 → 段落2 变蓝 */

记忆口诀:nth-child 数”所有兄弟排第几”,nth-of-type 数”同类型兄弟排第几”;带类型前缀(如 p:nth-child(2))还要求”它正好排第 2 且是 p”,否则选不到。

n 还能写表达式:2n(偶数)、2n+1(奇数)、3n+1(1、4、7…)、-n+3(前 3 个)。

3. ::before / ::after 必须配 content,默认 inline

1
2
3
4
5
.tip::before {
  content: "✓";          /* 必须有 content,否则伪元素不生成 */
  display: inline-block; /* 默认 inline,要宽高得改 display */
  margin-right: 8px;
}

content 是伪元素的灵魂:没有 content(或 content: "")就不生成盒子。伪元素默认 display: inline,想设宽高要先改成 block/inline-block/flex。

4. content 的高级玩法:attr() / counter / 字符串

1
2
3
4
5
6
7
8
9
/* 读 HTML 属性当内容:tooltip 经典用法 */
.tip::after { content: attr(data-tip); }

/* 计数器:自动编号 */
.list { counter-reset: sec; }
.item::before { counter-increment: sec; content: "第 " counter(sec) " 章: "; }

/* 拼接字符串 */
a::after { content: " (" attr(href) ")"; }

注意:content 只能放纯文本或 url(),不能放 HTML 标签;要复杂结构得靠背景图 / 数据 URI。

5. 表单伪类与 LVHA 顺序

链接伪类有书写顺序要求(四个优先级相同,后写的覆盖先写的):Link → Visited → Hover → Active(爱恨法则 “LoVe HA”)。

1
2
3
4
a:link    { color: blue; }
a:visited { color: purple; }
a:hover   { color: red; }
a:active  { color: orange; }

表单常用伪类:

1
2
3
4
5
input:required { border-left: 3px solid #e74c3c; }       /* 必填 */
input:valid   { border-color: #42b883; }                 /* 校验通过 */
input:invalid { border-color: #e74c3c; }                 /* 校验失败 */
/* 避免"一上来就标红":占位符还在时不算 invalid */
input:not(:placeholder-shown):invalid { background: #ffe0e0; }

⚠️ :visited 隐私限制:出于安全,:visited 只能改有限的颜色类属性(color、background-color、border-color 等),不能改布局、也不能被 JS 读取——所以”用 :visited 做用户浏览追踪”行不通。

6. 现代选择器 Level 4::has / :is / :where / :not

1
2
3
4
5
6
7
8
9
10
11
/* :has() —— 终于能"选中父元素"了(被它包含的某子元素满足条件) */
.card:has(img) { padding: 0; }        /* 含图片的卡片特殊处理 */

/* :is() —— 分组且取其中最高的优先级 */
:is(h1, h2, h3) { color: #222; }

/* :where() —— 永远 0 优先级,方便重置 */
:where(button, input) { margin: 0; }

/* :not() —— 排除 */
li:not(.active) { opacity: 0.6; }

:has() 是 2023 起广泛支持的”父选择器”,以前要用 JS 才能实现的逻辑现在纯 CSS 搞定;:where() 优先级为 0,特别适合写”怎么都盖不过的重置样式”。

其实你每天都在用

  • 表格斑马纹:tr:nth-child(even) { background: #f8f9fa; }
  • 按钮 hover 变色:a:hover / .btn:hover 是最常见的伪类
  • 列表项前加对勾:li::before { content: "✓"; }
  • 清除浮动(老写法):.clearfix::after { content:""; display:table; clear:both; }
  • 自定义复选框/单选:隐藏原生控件,用 .checkbox:checked + label::before 画新外观
  • 纯 CSS tooltip:::after + attr(data-tip) + :hover 显隐
  • 输入框聚焦高亮:input:focus 加 outline/box-shadow
  • 链接点过变紫::visited 自动生效

常见误解(FAQ)

❌ 误区一:”伪类和伪元素就是单冒号双冒号的区别”

表面是冒号数,本质是是否产生新盒子。伪类(:hover、:nth-child)只是给已有元素贴状态/位置标签,不产生节点;伪元素(::before、::first-line)会凭空造一个虚拟盒子,能设 display/position/宽高,参与布局。所以”双冒号”背后是”多了一个元素”。

❌ 误区二:”:nth-child(2) 就是选中第二个元素”

不准确。:nth-child(2) 是”在所有兄弟里排第 2“;若写成 p:nth-child(2),则要求”它既是第 2 个子元素、又是 p”,否则选不到。想”同类型里排第 2”要用 :nth-of-type(2)。混用是最高频的选不中 bug。

❌ 误区三:”::before 不加 content 也能显示”

不能。content 是伪元素生成的必要条件,没写 content(哪怕是 content: "")就不会产生盒子,自然什么都看不到。设了宽高却不显示,先检查 content。

❌ 误区四:”:visited 能随便改样式、还能 JS 读取做追踪”

不行。浏览器出于隐私,:visited 只允许改有限的颜色类属性(color、background-color、border-color 等),不能改尺寸/布局,计算样式里也读不到真实访问状态。想靠它做用户行为追踪在现代浏览器里走不通。

❌ 误区五:”伪元素能用 JS 直接操作”

伪元素不在 DOM 树里,document.querySelector('::before') 拿不到。只能读它的计算样式:getComputedStyle(el, '::before');要动态改它,通常借道 CSS 变量(在伪元素的 content/color 里用 var(),再用 JS 改变量)。

一句话总结

伪类(:)是给元素”贴状态/位置标签”、不产生新盒子;伪元素(::)是”凭空造虚拟节点”、必须配 content——分清”数的是所有兄弟还是同类兄弟”、记住 :visited 的隐私限制,你就能在面试里把这对选择器讲得明明白白。

本文由作者按照 CC BY 4.0 进行授权