CSS篇 | 选择器类型
前言
CSS 选择器是样式系统的入口——你写的每一条样式规则,第一步都是通过选择器”选中”元素。选择器的种类非常多,从最简单的标签选择器,到复杂的组合选择器、伪类、伪元素,再到近年来新增的 :has()、:is()、:where() 等现代选择器,构成了一个庞大的知识体系。
面试中,选择器相关的问题通常围绕这几个方向:
- “CSS 选择器优先级怎么计算?”
- “
!important什么时候该用?什么时候不该用?” - “说一下你知道的伪类和伪元素”
- “
:is()和:where()有什么区别?” - “CSS 选择器有性能问题吗?”
尤其是优先级计算,几乎是必考题。很多同学知道”ID > class > 标签”的大概规则,但一遇到具体场景就犯迷糊。今天我们就把选择器的分类和优先级彻底理清楚。
诊断自测
在开始之前,试着回答以下问题:
1. 以下两条规则,哪个优先级更高?
#header .nav li a { color: red; }
.main-nav > .nav-item > a.active { color: blue; }
2. :nth-child() 和 :nth-of-type() 有什么区别?
3. ::before 和 :before 有什么区别?
点击查看答案
第1题: 第一条规则优先级更高。
计算方法(ID, class, 标签):
#header .nav li a→ (1, 1, 2) — 1 个 ID,1 个 class,2 个标签.main-nav > .nav-item > a.active→ (0, 3, 1) — 0 个 ID,3 个 class,1 个标签
比较时先看 ID 数量:1 > 0,所以第一条规则优先级更高。
第2题:
:nth-child(n)匹配的是父元素下的第 n 个子元素,不区分标签类型:nth-of-type(n)匹配的是父元素下同类型标签中的第 n 个
比如 p:nth-child(2) 要求第 2 个子元素恰好是 <p> 才匹配,而 p:nth-of-type(2) 匹配的是第 2 个 <p> 元素(不管它在所有子元素中排第几)。
第3题: 功能上没有区别。CSS3 规范建议伪元素用双冒号 ::before,伪类用单冒号 :hover,以此来区分两者。但为了兼容性,浏览器同时支持 ::before 和 :before。
选择器分类
CSS 选择器可以分为以下几大类。
基础选择器
| 选择器 | 语法 | 示例 | 说明 |
|---|---|---|---|
| 通配符 | * | * { margin: 0; } | 匹配所有元素 |
| 标签(类型) | element | div { } | 匹配指定标签 |
| 类 | .class | .btn { } | 匹配 class 属性 |
| ID | #id | #header { } | 匹配 id 属性 |
| 属性 | [attr] | [disabled] { } | 匹配有该属性的元素 |
属性选择器的进阶用法:
/* 精确匹配 */
[type="text"] { }
/* 以某个值开头 */
[href^="https://"] { color: green; }
/* 以某个值结尾 */
[href$=".pdf"] { color: red; }
/* 包含某个值 */
[class*="icon-"] { }
/* 属性值匹配(不区分大小写) */
[type="submit" i] { }
组合选择器
| 选择器 | 语法 | 示例 | 说明 |
|---|---|---|---|
| 后代 | A B | .nav a { } | A 内部的所有 B(任意层级) |
| 子代 | A > B | .nav > li { } | A 的直接子元素 B |
| 相邻兄弟 | A + B | h1 + p { } | 紧跟在 A 后面的第一个 B |
| 通用兄弟 | A ~ B | h1 ~ p { } | A 后面的所有同级 B |
/* 后代选择器:所有层级 */
.card a { color: blue; }
/* 子代选择器:仅直接子元素 */
.card > .title { font-size: 18px; }
/* 相邻兄弟:紧邻的下一个 */
.alert + .alert { margin-top: 8px; }
/* 通用兄弟:后面的所有同级 */
.checked ~ .option { opacity: 0.5; }
伪类选择器
伪类表示元素的某种状态或位置,用单冒号 : 表示。
交互状态伪类:
a:link { } /* 未访问的链接 */
a:visited { } /* 已访问的链接 */
a:hover { } /* 鼠标悬停 */
a:active { } /* 鼠标按下 */
input:focus { } /* 获得焦点 */
input:focus-visible { } /* 键盘聚焦时才显示样式(推荐) */
input:focus-within { } /* 自身或后代获得焦点 */
结构伪类:
li:first-child { } /* 第一个子元素 */
li:last-child { } /* 最后一个子元素 */
li:nth-child(2n) { } /* 偶数位子元素 */
li:nth-child(3n+1) { } /* 第 1, 4, 7, ... 个 */
li:nth-last-child(2) { } /* 倒数第 2 个 */
p:first-of-type { } /* 同类型中的第一个 */
p:last-of-type { } /* 同类型中的最后一个 */
p:nth-of-type(odd) { } /* 同类型中的奇数位 */
:only-child { } /* 父元素只有一个子元素 */
:only-of-type { } /* 同类型中只有一个 */
:empty { } /* 没有子元素(包括文本节点) */
表单相关伪类:
input:checked { } /* 选中的 checkbox/radio */
input:disabled { } /* 禁用的表单元素 */
input:enabled { } /* 启用的表单元素 */
input:required { } /* 必填的表单元素 */
input:optional { } /* 非必填的表单元素 */
input:valid { } /* 验证通过的表单元素 */
input:invalid { } /* 验证失败的表单元素 */
input:placeholder-shown { } /* 显示着 placeholder 的输入框 */
否定伪类:
/* 排除某些元素 */
.list li:not(:last-child) {
border-bottom: 1px solid #eee;
}
button:not(:disabled) {
cursor: pointer;
}
伪元素选择器
伪元素创建一个”虚拟元素”,用双冒号 :: 表示。
/* 在元素内容前/后插入 */
.required::before {
content: '*';
color: red;
}
.link::after {
content: ' →';
}
/* 选中文本的样式 */
p::selection {
background: #1890ff;
color: #fff;
}
/* 首行和首字母 */
p::first-line {
font-weight: bold;
}
p::first-letter {
font-size: 2em;
float: left;
}
/* placeholder 样式 */
input::placeholder {
color: #999;
}
优先级计算规则
优先级(Specificity,也叫特异性)决定了当多条规则匹配同一个元素时,哪条规则的样式生效。
计算方式
优先级用一个三元组 (A, B, C) 表示:
- A:ID 选择器的数量
- B:类选择器、属性选择器、伪类选择器的数量
- C:标签选择器、伪元素选择器的数量
比较时从 A 到 C 依次比较,数值大的优先级高。
/* (0, 0, 1) — 1 个标签 */
p { color: black; }
/* (0, 1, 0) — 1 个 class */
.text { color: blue; }
/* (0, 1, 1) — 1 个 class + 1 个标签 */
p.text { color: green; }
/* (1, 0, 0) — 1 个 ID */
#title { color: red; }
/* (1, 1, 1) — 1 个 ID + 1 个 class + 1 个标签 */
#header .nav a { color: purple; }
几个容易搞混的细节
通配符 *、组合符 (>, +, ~)、:where() 不计入优先级。
/* (0, 0, 0) — 通配符没有优先级 */
* { color: gray; }
/* (0, 0, 1) — 组合符不计入 */
div > p { } /* 和 div p 优先级相同,都是 (0, 0, 2) */
:not()、:is()、:has() 本身不计入优先级,但它们参数中的选择器要计入。
/* (0, 1, 0) — :not 不算,但 .active 算一个 class */
:not(.active) { }
/* (1, 0, 0) — :is 不算,但 #main 算一个 ID */
:is(#main) .title { }
:where() 的优先级始终为 0,这是它和 :is() 的关键区别。
/* (0, 1, 0) — :is 继承参数中最高优先级 */
:is(.active) { color: red; }
/* (0, 0, 0) — :where 的优先级永远为 0 */
:where(.active) { color: blue; }
内联样式和 !important
在三元组之外,还有两个更高的优先级:
!important > 内联样式 > ID > class > 标签
- 内联样式:直接写在 HTML 的
style属性中,优先级高于所有选择器 !important:优先级最高,可以覆盖内联样式
<!-- 内联样式 -->
<p style="color: red;">文字</p>
/* 即使优先级再高,也覆盖不了内联样式 */
#main .text p { color: blue; } /* 无效 */
/* 除非用 !important */
p { color: green !important; } /* 生效 */
!important 的使用与滥用
什么时候可以用 !important?
- 覆盖第三方库的样式:当你无法修改第三方 CSS 时
- 工具类样式:如
.hidden { display: none !important; } - 临时调试:快速验证样式效果(记得事后移除)
什么时候不该用 !important?
- 日常开发中:如果你频繁使用
!important,说明你的 CSS 架构有问题 - 组件库样式:让使用者无法覆盖你的样式
- 与内联样式”对抗”:应该从源头减少内联样式的使用
!important 的优先级规则
当多个 !important 冲突时,按正常的优先级规则比较:
/* 当多个 !important 冲突时 */
p { color: red !important; } /* (0, 0, 1) */
.text { color: blue !important; } /* (0, 1, 0) — 生效 */
现代选择器::is()、:where()、:has()
这三个选择器是近几年加入规范的,极大地增强了 CSS 的表达能力。
:is() — 匹配列表
:is() 接受一个选择器列表,匹配其中任意一个。
/* 传统写法 */
.card h1,
.card h2,
.card h3 {
margin-top: 0;
}
/* :is() 写法 */
.card :is(h1, h2, h3) {
margin-top: 0;
}
:is() 的优先级取参数中最高的那个:
/* 优先级 = (1, 0, 0),因为 #title 的优先级最高 */
:is(.text, #title, p) { color: red; }
:where() — 零优先级的匹配列表
:where() 的功能和 :is() 完全一样,唯一的区别是优先级始终为 0。
/* 优先级 = (0, 0, 0),无论参数中写了什么 */
:where(#main, .header, div) { color: blue; }
这个特性在编写可覆盖的基础样式时非常有用:
/* 基础样式:用 :where() 保持低优先级,方便用户覆盖 */
:where(.btn) {
padding: 8px 16px;
border-radius: 4px;
}
/* 用户自定义:普通 class 就能覆盖 */
.btn {
padding: 12px 24px; /* 轻松覆盖 */
}
:has() — CSS 的”父选择器”
:has() 是 CSS 历史上最期待的特性之一。它可以根据子元素或后续元素的状态来选中父元素。
/* 选中包含 img 的 card */
.card:has(img) {
padding: 0;
}
/* 选中紧跟着 p 的 h2 */
h2:has(+ p) {
margin-bottom: 8px;
}
/* 表单验证:如果内部有 :invalid 的 input,给 form 加红边框 */
form:has(input:invalid) {
border: 2px solid red;
}
/* 选中没有子元素的容器 */
.container:has(> :first-child) {
/* 有子元素时的样式 */
}
.container:not(:has(> :first-child)) {
/* 空容器的样式 */
}
:has() 的浏览器支持已经很好了(Chrome 105+、Safari 15.4+、Firefox 121+),可以放心在新项目中使用。
实际案例:暗黑模式切换
/* 如果 html 标签上有 data-theme="dark" */
:root:has([data-theme="dark"]) {
--bg-color: #1a1a1a;
--text-color: #fff;
}
/* 如果用户系统偏好暗色 */
@media (prefers-color-scheme: dark) {
:root:not(:has([data-theme="light"])) {
--bg-color: #1a1a1a;
--text-color: #fff;
}
}
选择器性能
面试中偶尔会问到”CSS 选择器有性能问题吗?“,这里简单说一下。
浏览器的匹配方向
浏览器匹配选择器是从右往左的。比如 .nav li a,浏览器会先找到所有 <a> 元素,然后检查它们的祖先中是否有 <li>,再检查是否有 .nav。
为什么从右往左?因为从右往左可以快速排除大量不匹配的元素。如果从左往右,找到 .nav 后还要遍历它的所有后代,效率更低。
性能建议
- 避免使用通配符选择器(如
* { })作为关键选择器(最右侧的选择器) - 避免过深的嵌套:
.a .b .c .d .e { }需要层层检查祖先 - 避免使用标签选择器限定 class:
div.box { }不如.box { }直接
但说实话,在现代浏览器中,CSS 选择器的性能差异非常小。除非你的页面有几万个 DOM 节点且选择器写得极端复杂,否则不需要为选择器性能担心。
更值得关注的性能问题是:减少不必要的回流和重绘(下一章会讲)。
实际开发建议
/* 不推荐:嵌套太深 */
.page .content .article .section .paragraph a { }
/* 推荐:直接用有语义的 class */
.article-link { }
/* 不推荐:标签限定 class */
div.container { }
/* 推荐:直接用 class */
.container { }
常见误区
误区一:优先级可以”跨级”比较
很多人以为 10 个 class 的优先级就能超过 1 个 ID。这是错误的。
/* 11 个 class,优先级 (0, 11, 0) */
.a .b .c .d .e .f .g .h .i .j .k { color: blue; }
/* 1 个 ID,优先级 (1, 0, 0) */
#title { color: red; }
/* #title 胜出!因为 A 位置的 1 > 0,不管 B 位置多大 */
优先级的三个位置是独立比较的,不存在”256 个 class = 1 个 ID”这种换算关系。(早期有些文章用 1000/100/10 的权重来讲,这种说法是不准确的。)
误区二:伪类和伪元素的优先级相同
伪类(如 :hover、:nth-child())计入 B 类(class 级别),而伪元素(如 ::before、::after)计入 C 类(标签级别)。
/* (0, 1, 0) — :hover 是伪类,算 class 级别 */
a:hover { }
/* (0, 0, 1) — ::before 是伪元素,算标签级别 */
p::before { }
误区三::not() 本身有优先级
:not() 选择器本身的优先级为 0,但它参数中的选择器会正常计入优先级。
/* (0, 1, 1) — :not 不算,但 .disabled 和 button 各算一个 */
button:not(.disabled) { }
同理,:is() 和 :has() 本身也不计入优先级,但参数中的选择器会计入(取最高值)。
误区四:!important 能解决一切优先级问题
当多个 !important 冲突时,它们之间依然按照正常的优先级规则比较。而且 !important 会让样式变得极难维护——你加了一个,别人也加一个来覆盖你的,最终整个项目到处都是 !important,这就是所谓的”优先级军备竞赛”。
/* 优先级地狱 */
.title { color: red !important; }
#header .title { color: blue !important; } /* 这个会赢 */
小结
CSS 选择器是 CSS 的基石,掌握好选择器的分类和优先级计算,能让你的样式代码更加精准和可维护。
本章思维导图
- 基础选择器
- 通配符 *
- 标签选择器
- 类选择器 .class
- ID 选择器 #id
- 属性选择器 [attr]
- 组合选择器
- 后代 A B
- 子代 A > B
- 相邻兄弟 A + B
- 通用兄弟 A ~ B
- 伪类选择器
- 交互状态::hover, :focus, :active
- 结构::nth-child, :first-of-type, :empty
- 表单::checked, :disabled, :valid
- 否定::not()
- 伪元素选择器
- ::before / ::after
- ::selection
- ::first-line / ::first-letter
- ::placeholder
- 优先级(特异性)
- 三元组 (A, B, C)
- A: ID 数量
- B: class / 属性 / 伪类 数量
- C: 标签 / 伪元素 数量
- !important > 内联 > 选择器
- 不存在跨级比较
- 现代选择器
- :is()(匹配列表,继承最高优先级)
- :where()(匹配列表,零优先级)
- :has()(父选择器)
- 选择器性能
- 从右往左匹配
- 避免过深嵌套
- 现代浏览器差异极小
练习挑战
挑战一:基础(⭐)
计算以下选择器的优先级,并按优先级从高到低排序:
A: div.container p { }
B: .container > p.text { }
C: #main p { }
D: p { }
E: .text:hover { }
点击查看答案
- A:
div.container p→ (0, 1, 2) — 0 个 ID,1 个 class,2 个标签 - B:
.container > p.text→ (0, 2, 1) — 0 个 ID,2 个 class,1 个标签 - C:
#main p→ (1, 0, 1) — 1 个 ID,0 个 class,1 个标签 - D:
p→ (0, 0, 1) — 0 个 ID,0 个 class,1 个标签 - E:
.text:hover→ (0, 2, 0) — 0 个 ID,2 个(1 个 class + 1 个伪类),0 个标签
从高到低排序:C > B = E > A > D
B 和 E 的优先级相同(都是 (0, 2, 1) vs (0, 2, 0)),等等——其实 B 是 (0, 2, 1),E 是 (0, 2, 0),B 比 E 高一点。所以正确顺序是:C > B > E > A > D。
挑战二:进阶(⭐⭐)
以下 HTML 结构中,<a> 标签最终显示什么颜色?
<div id="nav" class="nav-bar">
<ul class="nav-list">
<li class="nav-item active">
<a href="#" class="nav-link">链接</a>
</li>
</ul>
</div>
#nav a { color: red; }
.nav-bar .nav-list .nav-item.active .nav-link { color: blue; }
.nav-link { color: green !important; }
a { color: orange; }
点击查看答案
最终显示 green(绿色)。
分析各规则优先级:
#nav a→ (1, 0, 1) → red.nav-bar .nav-list .nav-item.active .nav-link→ (0, 5, 0) → blue.nav-link→ (0, 1, 0) +!important→ greena→ (0, 0, 1) → orange
虽然规则 1 有 ID 选择器优先级最高,但规则 3 有 !important,!important 的优先级高于一切普通选择器(包括 ID 选择器和内联样式)。
所以结果是:!important 的绿色胜出。
如果规则 1 也加了 !important:#nav a { color: red !important; },那么两个 !important 之间按正常优先级比较,(1, 0, 1) > (0, 1, 0),红色会胜出。
挑战三:综合(⭐⭐⭐)
使用 :has()、:is() 和 :where() 重构以下 CSS,使代码更简洁,同时确保用户可以用一个简单的 .custom 类覆盖基础样式。
/* 原始代码 */
.card h1 { margin-top: 0; }
.card h2 { margin-top: 0; }
.card h3 { margin-top: 0; }
.card-with-image { padding: 0; }
.card-with-image .card-body { padding: 16px; }
.form-group.has-error .label { color: red; }
.form-group.has-error .input { border-color: red; }
.form-group.has-success .label { color: green; }
.form-group.has-success .input { border-color: green; }
点击查看答案
/* 基础样式用 :where() 包裹,保持零优先级,方便覆盖 */
/* 标题合并 */
:where(.card) :is(h1, h2, h3) {
margin-top: 0;
}
/* 利用 :has() 替代 .card-with-image 类 */
:where(.card):has(img) {
padding: 0;
}
:where(.card):has(img) .card-body {
padding: 16px;
}
/* 利用 :has() 根据 input 状态自动应用样式 */
:where(.form-group):has(input:invalid) .label {
color: red;
}
:where(.form-group):has(input:invalid) .input {
border-color: red;
}
:where(.form-group):has(input:valid) .label {
color: green;
}
:where(.form-group):has(input:valid) .input {
border-color: green;
}
/* 用户只需一个简单 class 就能覆盖 */
.custom {
margin-top: 16px; /* 轻松覆盖 :where() 的样式 */
}
关键点:
:is()合并了重复的标题选择器:has()让我们可以根据子元素状态来选中父元素,省去了手动添加状态类:where()确保基础样式的优先级为零,任何普通 class 都能覆盖:has(input:invalid)结合原生表单验证,无需 JavaScript 手动切换状态类
自我检测
读完本章后,确认你能回答以下问题:
- 能说出 CSS 选择器的四大分类(基础、组合、伪类、伪元素)
- 能用三元组 (A, B, C) 计算选择器的优先级
- 知道通配符
*、组合符和:where()不计入优先级 - 知道
:not()、:is()、:has()本身不计入优先级,但参数中的选择器会 - 能说出
:is()和:where()的区别(优先级不同) - 能举例说明
:has()的使用场景 - 知道
!important的正确使用场景和滥用的后果 - 知道伪类是 class 级别优先级,伪元素是标签级别优先级
- 知道浏览器从右往左匹配选择器
- 知道现代浏览器中选择器性能差异极小
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90