CSS 选择器与优先级
为什么选择器是 CSS 的第一道门
CSS 的全称是 Cascading Style Sheets——"层叠"二字暗示了一切。当多条规则同时命中一个元素时,浏览器需要一套精确的仲裁机制来决定"谁赢"。这套机制的核心就是 Specificity(优先级) 和 Cascade(层叠) 算法。
理解选择器不仅是写对样式的基础,更是调试样式冲突、设计可维护架构(如 BEM、ITCSS)的前提。在现代 CSS 中,:is()、:where()、:has() 等新伪类进一步改写了优先级的游戏规则。
面试定位:选择器优先级是 CSS 面试的经典题。面试官通过 specificity 计算、
!important的层叠位置、:is()与:where()的优先级差异等问题,判断候选人对 CSS 核心机制的理解深度。
选择器分类体系
基础选择器
| 选择器 | 语法 | Specificity | 说明 |
|---|---|---|---|
| 通用选择器 | * |
(0,0,0) | 匹配所有元素,不贡献优先级 |
| 类型选择器 | div, p |
(0,0,1) | 匹配元素标签名 |
| 类选择器 | .card |
(0,1,0) | 匹配 class 属性 |
| ID 选择器 | #header |
(1,0,0) | 匹配 id 属性 |
| 属性选择器 | [type="text"] |
(0,1,0) | 与类选择器同权重 |
组合选择器(Combinators)
/* 后代选择器(Descendant) */
.card p { color: gray; }
/* 子选择器(Child) */
.card > p { color: gray; }
/* 相邻兄弟选择器(Adjacent Sibling) */
h2 + p { margin-top: 0; }
/* 通用兄弟选择器(General Sibling) */
h2 ~ p { color: gray; }
/* 列组合选择器(Column Combinator,CSS Selectors 4) */
col.highlight || td { background: yellow; }
机制要点:组合符本身不贡献 Specificity,仅改变选择器的匹配逻辑。div p 与 div > p 的优先级相同(都是 (0,0,2)),但匹配范围不同。
伪类选择器(Pseudo-classes)
伪类描述元素的状态或结构位置,贡献 (0,1,0) 权重:
/* 用户交互状态 */
a:hover { color: red; }
input:focus { outline: 2px solid blue; }
button:active { transform: scale(0.98); }
/* 结构伪类 */
li:first-child { font-weight: bold; }
tr:nth-child(2n) { background: #f5f5f5; }
p:last-of-type { margin-bottom: 0; }
/* 表单状态伪类 */
input:required { border-color: red; }
input:valid { border-color: green; }
input:disabled { opacity: 0.5; }
伪元素选择器(Pseudo-elements)
伪元素创建文档树中不存在的虚拟元素,贡献 (0,0,1) 权重:
p::first-line { font-variant: small-caps; }
p::first-letter { font-size: 2em; float: left; }
.quote::before { content: "\201C"; }
.quote::after { content: "\201D"; }
input::placeholder { color: #999; }
::selection { background: #b3d4fc; }
规范说明:CSS3 使用双冒号 :: 区分伪元素和伪类(单冒号 :),但浏览器为兼容仍接受单冒号的 ::before、::after 等写法。
Specificity 计算模型
三元组模型 (A, B, C)
CSS Specificity 使用三个维度的权重:
| 维度 | 来源 | 权重表示 |
|---|---|---|
| A | ID 选择器数量 | (1,0,0) |
| B | 类选择器、属性选择器、伪类数量 | (0,1,0) |
| C | 类型选择器、伪元素数量 | (0,0,1) |
比较规则:从左到右逐位比较。(1,0,0) 永远大于 (0,99,99)——这不是十进制运算。
计算示例
/* (0,0,1) —— 一个类型选择器 */
p { color: black; }
/* (0,1,1) —— 一个类 + 一个类型 */
p.intro { color: gray; }
/* (0,2,1) —— 两个类 + 一个类型 */
div.card.featured { border: 2px solid gold; }
/* (1,0,1) —— 一个 ID + 一个类型 */
#sidebar p { font-size: 14px; }
/* (1,1,3) —— 一个 ID + 一个类 + 三个类型 */
#main div.content ul li { list-style: none; }
/* (0,1,1) —— 伪类贡献 B 维度 */
a:hover { text-decoration: underline; }
/* (0,0,2) —— 伪元素贡献 C 维度 */
p::first-line { font-weight: bold; }
特殊规则
内联样式:style 属性的优先级高于所有选择器,相当于 (1,0,0,0) 的四维表示。
!important:不参与 Specificity 计算,而是将声明提升到 Cascade 的更高层级。两个 !important 声明之间仍按正常 Specificity 比较。
/* 正常层叠中:#id 赢 */
p { color: red !important; } /* !important 层 */
#main p { color: blue; } /* 正常层 */
/* 结果:red(!important 层级更高) */
/* 两个 !important 比较 specificity */
p { color: red !important; } /* (0,0,1) */
.text { color: blue !important; } /* (0,1,0) */
/* 结果:blue(同为 !important,比 specificity) */
现代伪类选择器
:is() —— 宽容选择器列表
:is() 接受一个选择器列表,匹配列表中任意一个。它的核心特性是:取参数中最高的 Specificity。
/* 传统写法 */
header a:hover,
main a:hover,
footer a:hover {
color: red;
}
/* :is() 写法 */
:is(header, main, footer) a:hover {
color: red;
}
/* Specificity: (0,1,1) —— :is() 取最高参数的权重 (0,0,1) + a(0,0,1) + :hover(0,1,0) */
宽容解析(Forgiving Selector List):如果列表中某个选择器无效,不会导致整条规则失效——这与传统逗号分隔的选择器列表行为不同。
:where() —— 零优先级选择器
:where() 语法与 :is() 完全相同,唯一区别是 Specificity 始终为 (0,0,0):
/* 基础样式层——容易被覆盖 */
:where(.card) {
padding: 1rem;
border: 1px solid #ddd;
}
/* 用户自定义样式——轻松覆盖 */
.card {
padding: 2rem; /* 赢,因为 (0,1,0) > (0,0,0) */
}
工程价值::where() 是构建低优先级基础样式库的利器。CSS Reset、组件库的默认样式层非常适合使用 :where() 包裹,让用户无需 !important 即可覆盖。
:has() —— 父选择器(关系伪类)
:has() 是 CSS 历史上最具突破性的选择器,实现了"根据后代/兄弟状态选择祖先"的能力:
/* 包含图片的 card 使用不同布局 */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
/* 包含无效输入的 form 显示错误边框 */
form:has(input:invalid) {
border: 2px solid red;
}
/* 相邻兄弟关系 */
h2:has(+ p) {
margin-bottom: 0.5rem;
}
/* 否定关系 */
.card:not(:has(img)) {
padding: 2rem;
}
性能注意::has() 的匹配方向与传统选择器相反(从祖先出发检查后代),浏览器需要额外的 invalidation 机制。避免 :has() 与大范围通用选择器组合(如 *:has(.foo))。
浏览器支持(2025):Chrome 105+、Safari 15.4+、Firefox 121+。Baseline 2023 已达成。
:not() —— 否定伪类
:not() 在 Selectors Level 4 中支持复杂选择器列表:
/* Level 3: 仅支持简单选择器 */
p:not(.intro) { color: gray; }
/* Level 4: 支持选择器列表 */
p:not(.intro, .summary) { color: gray; }
/* Specificity: 取参数中最高权重 */
p:not(#special) { color: gray; }
/* (1,0,1) —— :not() 取 #special 的权重 */
Cascade 层叠算法
完整的层叠顺序(Cascade Sorting Order)
当多条规则匹配同一元素时,浏览器按以下优先级排序(从高到低):
- Transition 声明
- !important + 用户代理样式
- !important + 用户样式
- !important + 作者样式
- Animation 声明
- 作者样式(按
@layer→ 无 layer → inline style 排序) - 用户样式
- 用户代理样式(UA stylesheet)
@layer —— Cascade Layers
CSS @layer 是 Cascade Level 5 引入的分层机制,让开发者显式控制层叠顺序:
/* 声明层顺序:先声明的优先级更低 */
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer base {
body { font-family: system-ui; line-height: 1.6; }
a { color: var(--link-color); }
}
@layer components {
.btn { padding: 0.5rem 1rem; border-radius: 4px; }
}
@layer utilities {
.mt-4 { margin-top: 1rem; }
}
关键规则:
- 后声明的 layer 优先级更高
- 不在任何 layer 中的样式,优先级高于所有 layer
!important在 layer 中的行为反转:先声明的 layer 的!important优先级更高@layer与@import配合使用:@import url("lib.css") layer(lib);
/* !important 反转示例 */
@layer base, theme;
@layer base {
.btn { color: black !important; } /* 这条赢! */
}
@layer theme {
.btn { color: red !important; }
}
/* 结果:black。因为 !important 在 layer 中优先级反转 */
选择器匹配性能
浏览器的右到左匹配
浏览器引擎(如 Blink、Gecko)从选择器的最右侧(key selector)开始匹配,然后向左验证祖先链:
/* 浏览器先找所有 <li>,再检查是否在 .nav > ul 内 */
.nav > ul > li { color: blue; }
性能建议
- 避免过深的选择器嵌套:
.header .nav .menu .item .link不仅难维护,还增加匹配开销 - 避免通用选择器作为 key selector:
div > *会匹配页面上所有元素 - ID 选择器无需限定:
div#header中的div是冗余的 - 现代浏览器已高度优化:选择器性能在大多数场景下不是瓶颈,可维护性优先
实战场景
场景一:组件库的样式覆盖策略
/* 组件库内部:使用 :where() 降低优先级 */
:where(.ui-button) {
background: #007bff;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
:where(.ui-button:hover) {
background: #0056b3;
}
/* 用户项目中:简单类选择器即可覆盖 */
.ui-button {
background: #28a745; /* 覆盖成功,无需 !important */
}
场景二:使用 :has() 实现响应式卡片
/* 有图片时:水平布局 */
.card:has(> img) {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1rem;
}
/* 无图片时:垂直堆叠 */
.card:not(:has(> img)) {
display: flex;
flex-direction: column;
}
/* 最后一个 card 无底部边距 */
.card:has(+ .card) {
margin-bottom: 1rem;
}
.card:not(:has(+ .card)) {
margin-bottom: 0;
}
场景三:表单验证状态联动
/* 表单有无效字段时,禁用提交按钮的视觉 */
form:has(:invalid) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}
/* 所有字段有效时高亮提交按钮 */
form:not(:has(:invalid)) button[type="submit"] {
background: #28a745;
color: white;
}
面试高频问题
Q: 如何计算 CSS Specificity?!important 在哪个层级?
回答要点:Specificity 使用 (A, B, C) 三元组——A 是 ID 选择器数量、B 是类/属性/伪类数量、C 是类型/伪元素数量。从左到右逐位比较,不进位。!important 不参与 Specificity 计算,而是将声明提升到 Cascade 的更高层级。两个 !important 声明之间仍按正常 Specificity 比较。内联样式高于所有选择器但低于 !important。
Q: :is() 和 :where() 的区别?各适合什么场景?
回答要点:语法完全相同,唯一区别是 Specificity::is() 取参数列表中最高的权重,:where() 始终为零。:is() 适合简化重复选择器的书写;:where() 适合构建低优先级的基础样式层(如 CSS Reset、组件库默认样式),让消费者轻松覆盖。
Q: :has() 选择器有什么限制?
回答要点::has() 不能嵌套使用(:has(:has()) 无效),不能出现在伪元素中,不能用在 @keyframes 内。性能方面,应避免与宽泛选择器组合。它的匹配方向与传统选择器相反,浏览器需要反向 invalidation,所以在超大 DOM 中应审慎使用。
Q: @layer 的层叠顺序和 !important 的反转机制?
回答要点:@layer 的声明顺序决定优先级——后声明的 layer 优先级更高,不在 layer 中的样式优先级最高。但 !important 在 layer 中的行为反转:先声明的 layer 中的 !important 优先级反而更高。这个设计是为了让基础层(如 reset)的 !important 防御性声明不被上层轻易覆盖。