CSS 选择器与优先级

深入理解 CSS 选择器的匹配机制、优先级(Specificity)计算模型、Cascade 层叠算法,以及 :is()、:where()、:has() 等现代伪类选择器的工程用法。

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 pdiv > 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)

当多条规则匹配同一元素时,浏览器按以下优先级排序(从高到低):

  1. Transition 声明
  2. !important + 用户代理样式
  3. !important + 用户样式
  4. !important + 作者样式
  5. Animation 声明
  6. 作者样式(按 @layer → 无 layer → inline style 排序)
  7. 用户样式
  8. 用户代理样式(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; }

性能建议

  1. 避免过深的选择器嵌套.header .nav .menu .item .link 不仅难维护,还增加匹配开销
  2. 避免通用选择器作为 key selectordiv > * 会匹配页面上所有元素
  3. ID 选择器无需限定div#header 中的 div 是冗余的
  4. 现代浏览器已高度优化:选择器性能在大多数场景下不是瓶颈,可维护性优先

实战场景

场景一:组件库的样式覆盖策略

/* 组件库内部:使用 :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 防御性声明不被上层轻易覆盖。


延展阅读