三:详解 CSS 选择器

一、选择器:CSS 的第一个核心机制

在入门阶段,你学会了三种基本选择器:标签选择器(p)、类选择器(.class)、ID 选择器(#id)。它们能覆盖 80% 的日常需求。

但剩下的 20%,才是区分“能写样式”和“真正理解 CSS”的分水岭。当你的样式莫名其妙地不生效时,当你在浏览器开发者工具里看到一条被划掉的样式规则时,当你用 !important 强行覆盖却发现还是不行时——这些问题的答案,都藏在一个概念里:选择器的优先级

本篇的目标:

  1. 彻底理解选择器优先级的计算规则(specificity)
  2. 掌握组合选择器:后代、子代、相邻兄弟、通用兄弟
  3. 理解伪类和伪元素的本质区别
  4. 学会使用属性选择器进行精确匹配
  5. 知道为什么你的样式有时“不生效”,以及如何系统性地排查

二、优先级:CSS 世界的“权力排行”

CSS 的全称是“层叠样式表”(Cascading Style Sheets)。“层叠”意味着多条规则可以同时作用于同一个元素,浏览器需要一套规则来决定最终采用哪一条。这套规则就是优先级

优先级的四个等级

W3C 规范将选择器的优先级量化为一个四元组 (a, b, c, d)

等级 对应选择器 示例 权重
a 内联样式(style 属性) style="color: red" 最高,直接写在元素上
b ID 选择器 #header 每个 ID 计 1
c 类选择器、属性选择器、伪类 .card[type="text"]:hover 每个计 1
d 标签选择器、伪元素 divp::before 每个计 1

比较规则:从左到右逐级比较,a 大的直接胜出;a 相同则比 b,以此类推。注意:这不是十进制数字(不是 1000、100、10、1 的关系),而是四个独立的级别。10 个类选择器也赢不了 1 个 ID 选择器。

计算实例

/* 选择器                 a  b  c  d */
/* #sidebar .active      0  1  1  0 */
/* .card p               0  0  1  1 */
/* div p span            0  0  0  3 */
/* #main #content .box   0  2  1  0 */

如果两个规则作用于同一个元素且优先级相同,写在后面的规则胜出(这就是“层叠”的本意)。

!important:核武器,慎用

!important 会绕过正常的优先级计算,直接强制采用该声明:

p {
  color: red !important;
}

!important 本身也有优先级:在同一个元素上,多个 !important 声明之间仍然按照 (a, b, c, d) 规则比较。更麻烦的是,一旦用了 !important,日后想覆盖它就非常困难,往往只能用更强的 !important 去覆盖,形成恶性循环。

原则!important 应该只在两种情况下使用——覆盖第三方库的样式(你无法修改源码)、或者作为临时调试手段。日常开发中,通过提高选择器精度来解决问题,而不是祭出 !important

三、组合选择器:精准打击的武器

单个选择器的能力有限。真正的威力来自组合。

后代选择器:空格

选择某个元素内部的所有匹配后代,无论嵌套多深。

/* .article 内部的所有 p 元素,不管嵌套多少层 */
.article p {
  line-height: 1.8;
}
<div class="article">
  <p>直接子元素,会被选中。</p>
  <div>
    <p>嵌套了两层,仍然会被选中。</p>
  </div>
</div>

子代选择器:>

只选择直接子元素,不穿透到更深层级。

/* 只选中 .card 的直接子元素 div */
.card > div {
  margin-bottom: 10px;
}

后代 vs 子代的区别很重要:用后代选择器(空格)容易误伤深层嵌套的元素,能明确边界时尽量用子代选择器。

相邻兄弟选择器:+

选择紧跟在某个元素之后第一个兄弟元素。

/* h2 后面紧跟着的第一个 p */
h2 + p {
  font-weight: bold;
}
<h2>标题</h2>
<p>这个段落会被选中(紧跟在 h2 之后)。</p>
<p>这个段落不会被选中(不是紧跟着的)。</p>

常用场景:标题后面第一个段落的样式特殊处理。

通用兄弟选择器:~

选择某个元素之后所有同级的兄弟元素。

/* h2 之后所有的同级 p 元素 */
h2 ~ p {
  color: gray;
}
<h2>标题</h2>
<p>被选中。</p>
<div>其他元素</div>
<p>也被选中(在 h2 之后,且同级)。</p>

组合选择器也会影响优先级

组合符号(空格、>+~)本身不贡献优先级。优先级只看最终的选择器部分。例如 .card > div 的优先级等同于 .card div,都是 (0, 0, 1, 1)。

四、伪类与伪元素:看似相似,本质不同

这是 CSS 中最容易被混淆的概念之一。它们都带冒号,但做的事情完全不同。

伪类(Pseudo-class):选择元素的特定状态

伪类前面用一个冒号 :。它根据元素的状态来筛选,而不是在 DOM 中真实存在的元素。

/* 用户交互状态 */
a:hover { }        /* 鼠标悬停 */
a:visited { }      /* 已访问过 */
input:focus { }    /* 获得焦点(正在输入) */

/* 位置状态 */
li:first-child { } /* 作为第一个子元素 */
li:last-child { }  /* 作为最后一个子元素 */
li:nth-child(2) { }  /* 作为第 2 个子元素 */
li:nth-child(odd) { }  /* 作为奇数个子元素 */
li:nth-child(2n+1) { } /* 同上,更灵活的写法 */

/* 否定伪类 */
p:not(.special) { } /* 没有 .special 类的段落 */

:nth-child() 详解:这是最强大的位置伪类。括号里可以写:

  • 数字::nth-child(3)——第 3 个
  • 关键字::nth-child(odd)(奇数)、:nth-child(even)(偶数)
  • 公式::nth-child(2n+1)——n 从 0 开始递增:1, 3, 5, 7…(等同于 odd)
  • 公式::nth-child(3n)——3, 6, 9, 12…

容易误解的地方:nth-child() 找的是“在所有兄弟中排第几个”,而不是“在所有同类兄弟中排第几个”。比如 p:nth-child(2) 的意思是“作为父元素第二个子元素,且标签是 p”。如果父元素的第二个子元素是 div,这个选择器就匹配不到任何东西。

伪元素(Pseudo-element):创建不存在的虚拟元素

伪元素前面用两个冒号 ::(旧规范也接受单冒号,但双冒号是现代标准写法)。它相当于在 DOM 中凭空插入了一个虚拟标签,用来装饰。

/* 在元素内容前后插入装饰 */
.box::before {
  content: "→ ";         /* 必须写 content 属性,否则伪元素不出现 */
  color: red;
}

.box::after {
  content: " ←";
  color: blue;
}

/* 选中用户选中的文字 */
::selection {
  background: yellow;
  color: black;
}

/* 输入框的占位文字 */
input::placeholder {
  color: #ccc;
  font-style: italic;
}

::before::after 的核心要点:

  • content 属性是必须的:即使内容为空也要写 content: "";,否则伪元素不会生成。
  • 伪元素默认是行内元素,如果需要设置宽高,要加 display: blockdisplay: inline-block
  • 它们成为该元素的“第一个子元素”(::before)和“最后一个子元素”(::after)。

伪类 vs 伪元素速记:

  伪类 伪元素
写法 单冒号 : 双冒号 ::
作用对象 已有元素的状态 创建虚拟元素
是否增加 DOM 创建虚拟节点(不在 DOM 中)
典型案例 :hover:first-child ::before::after

五、属性选择器:不看标签,看属性

属性选择器允许你根据元素的属性来匹配,而不必依赖 class 或 id。

基本语法

写法 含义 示例
[attr] 拥有该属性即可 [disabled] 匹配所有带 disabled 属性的元素
[attr="value"] 属性值完全等于 [type="submit"] 匹配提交按钮
[attr^="value"] 属性值以此开头 [href^="https"] 匹配所有 https 链接
[attr$="value"] 属性值以此结尾 [href$=".pdf"] 匹配所有 PDF 链接
[attr*="value"] 属性值包含此字符串 [class*="card"] 匹配 class 中包含 “card” 的元素

实战用法

/* 给所有外部链接加特殊样式 */
a[href^="http"]:not([href*="example.com"])::after {
  content: " ↗";
  font-size: 12px;
}

/* 匹配不同类型的输入框 */
input[type="text"] {
  border: 1px solid #ccc;
}
input[type="submit"] {
  background: #4a90d9;
  color: white;
}

/* 匹配所有空链接(href="#") */
a[href="#"] {
  cursor: default;
  color: gray;
}

属性选择器的优先级与类选择器同级(c 级)。

六、为什么你的样式“不生效”?常见排查清单

每个前端开发者都会遇到这种情况:明明写了样式,浏览器里却看不到效果。以下是一个系统性的排查清单:

1. 选择器写错了

  • 类选择器忘了点号:card { } 应该是 .card { }
  • ID 选择器忘了井号:header { } 应该是 #header { }
  • 标签名拼写错误。

2. 优先级不够

你的 .card .title { } 被前面的 #sidebar .title { } 覆盖了。打开 F12 开发者工具,看被划掉的那条规则,上面通常有另一条优先级更高的规则生效。这是最常见的“不生效”原因。

3. 属性对元素无效

某些 CSS 属性对某些元素不起作用:

  • 给行内元素(<span>)设置 widthheight 无效(除非它是行内替换元素如 <img>)。
  • z-index 只对定位元素(position 不为 static)有效。
  • ::before 没有写 content 属性,伪元素根本不生成。

4. 继承 vs 非继承

有些属性会自动继承自父元素(如 colorfont-family),有些不会(如 borderpaddingmargin)。你不能期望给 <body> 设置 border 然后所有内部元素都有边框。

可以强制继承:border: inherit;,但通常这不是好做法。

5. 层叠顺序问题

后面的规则覆盖前面的规则。如果你有两个 .box { } 定义在不同的 CSS 文件中,后加载的那个会覆盖先加载的(在优先级相同的情况下)。

6. 简写属性覆盖了长写属性

这是一个隐蔽的陷阱:

.box {
  background-color: red;    /* 没用——被下面的 background 覆盖了 */
  background: url(bg.jpg);  /* background 简写会重置所有背景相关属性 */
}

简写属性(如 backgroundfontmargin)会覆盖同一类别中之前设置的长写属性。解决方案:要么把长写属性放在简写之后,要么始终使用同一种写法。

七、本篇综合示例:用选择器做一张斑马纹表格

下面这段代码综合运用了本篇学到的多个选择器技巧,请手敲并观察效果:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>选择器综合演示</title>
  <style>
    /* 基础表格样式 */
    table {
      width: 100%;
      border-collapse: collapse;
      font-family: sans-serif;
    }

    thead th {
      background: #4a90d9;
      color: white;
      padding: 12px 16px;
      text-align: left;
    }

    /* 斑马纹:偶数行 */
    tbody tr:nth-child(even) {
      background: #f5f7fa;
    }

    /* 鼠标悬停行 */
    tbody tr:hover {
      background: #e8f0fe;
    }

    /* 最后一个单元格右对齐 */
    td:last-child {
      text-align: right;
      font-weight: bold;
    }

    /* 状态列的颜色 */
    td:nth-child(3) {
      font-size: 13px;
    }

    /* 通过属性选择器给不同状态加颜色 */
    [data-status="完成"] {
      color: #27ae60;
    }

    [data-status="进行中"] {
      color: #e67e22;
    }

    [data-status="未开始"] {
      color: #999;
    }

    /* 外部链接加标识 */
    a[href^="http"]:not([href*="example.com"])::after {
      content: " ↗";
      font-size: 11px;
      color: #999;
    }

    td, th {
      padding: 10px 16px;
      border-bottom: 1px solid #e0e0e0;
    }
  </style>
</head>
<body>

  <table>
    <thead>
      <tr>
        <th>任务名称</th>
        <th>负责人</th>
        <th>状态</th>
        <th>工时</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>重构首页布局</td>
        <td>张三</td>
        <td data-status="完成">完成</td>
        <td>8h</td>
      </tr>
      <tr>
        <td>开发搜索功能</td>
        <td>李四</td>
        <td data-status="进行中">进行中</td>
        <td>16h</td>
      </tr>
      <tr>
        <td>编写用户文档</td>
        <td>王五</td>
        <td data-status="未开始">未开始</td>
        <td>4h</td>
      </tr>
      <tr>
        <td>性能优化</td>
        <td>张三</td>
        <td data-status="进行中">进行中</td>
        <td>12h</td>
      </tr>
      <tr>
        <td>修复登录 Bug</td>
        <td>李四</td>
        <td data-status="完成">完成</td>
        <td>2h</td>
      </tr>
    </tbody>
  </table>

</body>
</html>

选择器清单(回顾本篇知识点):

  • thead th:后代选择器,选中表头中的 th。
  • tbody tr:nth-child(even):伪类选择器,偶数行加灰色背景(斑马纹)。
  • tbody tr:hover:伪类选择器,鼠标悬停高亮。
  • td:last-child:伪类选择器,最后一列右对齐。
  • td:nth-child(3):伪类选择器,第三列(状态列)特殊字号。
  • [data-status="完成"]:属性选择器,精确匹配状态值为“完成”的元素。
  • a[href^="http"]:not([href*="example.com"])::after:组合了属性选择器 + 否定伪类 + 伪元素,给外部链接自动加箭头图标。

八、本篇小结

这一篇我们深入了 CSS 选择器的世界:

  • 优先级 (a, b, c, d):内联、ID、类/属性/伪类、标签/伪元素,逐级比较。10 个类也赢不了 1 个 ID。
  • 组合选择器:空格(后代)、>(子代)、+(相邻兄弟)、~(通用兄弟),让你精准锁定目标元素。
  • 伪类 vs 伪元素:前者选择已有元素的特定状态(:hover:nth-child),后者创建虚拟元素(::before::after 必须配合 content)。
  • 属性选择器[attr][attr=""][attr^=""][attr$=""][attr*=""],不看标签只看属性。
  • 排查样式不生效:检查选择器拼写、优先级、属性适用范围、继承特性、层叠顺序、简写覆盖。

选择器是 CSS 的入口。只有精确地选中了目标元素,后面的样式声明才有意义。把这一篇的内容吃透,你在写 CSS 时会有一种“指哪打哪”的掌控感。

下一篇预告

下一篇,我们将深入 CSS 最核心的机制——如何确定一个属性的值? 从“我写了这个样式”到“浏览器最终用哪个值”,中间经历了层叠、继承、初始值、回退等多个步骤。理解了这个流程,你就能解释几乎所有“为什么这个样式没生效”的问题。

前端,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容