跳到主要内容
预计阅读 37 分钟

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; }匹配所有元素
标签(类型)elementdiv { }匹配指定标签
.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 + Bh1 + p { }紧跟在 A 后面的第一个 B
通用兄弟A ~ Bh1 ~ 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?

  1. 覆盖第三方库的样式:当你无法修改第三方 CSS 时
  2. 工具类样式:如 .hidden { display: none !important; }
  3. 临时调试:快速验证样式效果(记得事后移除)

什么时候不该用 !important?

  1. 日常开发中:如果你频繁使用 !important,说明你的 CSS 架构有问题
  2. 组件库样式:让使用者无法覆盖你的样式
  3. 与内联样式”对抗”:应该从源头减少内联样式的使用

!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 后还要遍历它的所有后代,效率更低。

性能建议

  1. 避免使用通配符选择器(如 * { })作为关键选择器(最右侧的选择器)
  2. 避免过深的嵌套.a .b .c .d .e { } 需要层层检查祖先
  3. 避免使用标签选择器限定 classdiv.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 的基石,掌握好选择器的分类和优先级计算,能让你的样式代码更加精准和可维护。

本章思维导图

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(绿色)

分析各规则优先级:

  1. #nav a → (1, 0, 1) → red
  2. .nav-bar .nav-list .nav-item.active .nav-link → (0, 5, 0) → blue
  3. .nav-link → (0, 1, 0) + !important → green
  4. a → (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() 的样式 */
}

关键点:

  1. :is() 合并了重复的标题选择器
  2. :has() 让我们可以根据子元素状态来选中父元素,省去了手动添加状态类
  3. :where() 确保基础样式的优先级为零,任何普通 class 都能覆盖
  4. :has(input:invalid) 结合原生表单验证,无需 JavaScript 手动切换状态类

自我检测

读完本章后,确认你能回答以下问题:

  • 能说出 CSS 选择器的四大分类(基础、组合、伪类、伪元素)
  • 能用三元组 (A, B, C) 计算选择器的优先级
  • 知道通配符 *、组合符和 :where() 不计入优先级
  • 知道 :not():is():has() 本身不计入优先级,但参数中的选择器会
  • 能说出 :is():where() 的区别(优先级不同)
  • 能举例说明 :has() 的使用场景
  • 知道 !important 的正确使用场景和滥用的后果
  • 知道伪类是 class 级别优先级,伪元素是标签级别优先级
  • 知道浏览器从右往左匹配选择器
  • 知道现代浏览器中选择器性能差异极小

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90