什么是伪类和伪元素?它们之间有什么区别?
伪类和伪元素都是 CSS 选择器的扩展机制,但本质不同:
伪类(单冒号 :):选择 DOM 中已有元素的某种状态。元素本身存在,伪类只是在特定条件下"筛选"它。比如 :hover 选中的还是那个 <a> 元素,只不过它正处于鼠标悬停状态。
伪元素(双冒号 ::):在 DOM 树中创建一个不存在的虚拟节点,然后对这个虚拟节点施加样式。比如 ::before 在元素内容前面插入一个匿名盒子,这个盒子在 HTML 源码里根本不存在。
用一个类比:伪类是给已有的人拍一张特定状态的照片(站着、坐着),伪元素则是凭空造出一个不存在的人再给他拍照。
css/* 伪类:选择处于悬停状态的 a 元素 */ a:hover { color: red; } /* 伪元素:在元素内容前插入虚拟内容 */ a::before { content: "→ "; }
语法规范的演变
CSS1 和 CSS2 时代,伪类和伪元素都用单冒号 :,比如 :before、:after、:first-letter 和 :hover、:focus 混在一起,容易混淆。
CSS3 为了区分二者,规定:
- 伪类继续用单冒号
:hover - 伪元素改用双冒号
::before
浏览器为了向后兼容,仍然支持 :before 这种单冒号写法,但在新项目中应该统一使用双冒号。
常见伪类分类
| 类别 | 示例 | 说明 |
|---|---|---|
| 交互状态 | :hover :active :focus :focus-within | 用户交互触发的状态 |
| 位置匹配 | :first-child :last-child :nth-child(n) :nth-of-type(n) | 基于元素在兄弟中的位置 |
| 否定与匹配 | :not() :is() :where() :has() | 逻辑组合选择器 |
| 表单相关 | :checked :disabled :valid :invalid :required | 表单元素的状态 |
| 链接状态 | :link :visited | 未访问/已访问链接 |
常见伪元素分类
| 伪元素 | 作用 |
|---|---|
::before / ::after | 在元素内容前后插入虚拟盒子 |
::first-letter | 选中块级元素首字母(可实现首字下沉效果) |
::first-line | 选中块级元素首行(字号变化时首行范围自适应) |
::selection | 用户选中文本的样式 |
::placeholder | 输入框占位文字的样式 |
选择器组合规则
一个选择器可以同时使用多个伪类,它们叠加生效:
css/* 合法:同时匹配"第一个子元素"和"悬停状态" */ li:first-child:hover { background: yellow; }
但一个选择器只能使用一个伪元素,且伪元素必须出现在选择器末尾:
css/* 合法:伪元素在最后 */ a:hover::before { content: "🔗"; } /* 非法:伪元素后面不能再接伪类 */ a::before:hover { } /* 无效 */
原因是伪元素创建了一个新的虚拟盒子,它不是 DOM 节点,无法拥有状态,所以 ::before:hover 没有意义。
渲染层面的差异
伪类不影响渲染树的构建——它只是让选择器在匹配阶段多了一个条件,匹配到的元素照常进入渲染树。
伪元素则会在渲染树中额外生成一个匿名盒子。浏览器在布局计算时,::before 和 ::after 生成的盒子会参与父元素的排版,占用空间(如果设置了宽高或内容的话)。你可以打开 DevTools 的 Elements 面板,看到 ::before 和 ::after 出现在元素节点下方。
追问
:nth-child(n) 和 :nth-of-type(n) 有什么区别?
:nth-child(n) 在所有兄弟元素中数第 n 个,不管标签类型。:nth-of-type(n) 只在同标签类型的兄弟中数第 n 个。
html<div> <h2>标题</h2> <p>第一段</p> <!-- p:nth-child(2) 匹配失败:它是第2个子元素但不是h2 --> <p>第二段</p> <!-- p:nth-of-type(2) 匹配成功:它是第2个p --> </div>
关键记忆:nth-child 先数位置再验类型,nth-of-type 先筛类型再数位置。
::before 和 ::after 必须配合 content 属性吗?
是的。没有 content 属性,伪元素不会生成盒子。哪怕不需要任何文字内容,也必须写 content: ''。content 支持的值包括字符串、attr() 函数、图片 url()、计数器 counter() 等:
css.tooltip::after { content: attr(data-tip); /* 读取元素属性作为内容 */ } .counter::before { content: counter(section) ". "; /* 配合 counter-increment 使用 */ }
伪元素可以绑定 JS 事件吗?
不能。伪元素不存在于 DOM 树中,document.querySelector 无法选中它,JS 事件也无法直接绑定。如果想间接检测伪元素的点击,可以通过判断 event.offsetX / event.offsetY 是否落在伪元素的渲染区域内。实际开发中更推荐用真实 DOM 元素替代伪元素来实现交互需求。
:has() 伪类和伪元素有什么关系?
:has() 是 CSS4 引入的关系型伪类(不是伪元素),被称为"父选择器"。它可以根据子元素的状态来选择父元素:
css/* 选择包含 img 子元素的 a 标签 */ a:has(> img) { border: 1px solid #ccc; } /* 选择后面紧跟着 h2 的 h1(兄弟关系) */ h1:has(+ h2) { margin-bottom: 0; }
:has() 的出现弥补了 CSS 长期缺失的"向上选择"能力,目前主流浏览器已全面支持。