【前端小技巧】你可能不知道的CSS选择器::is(), :where(), :has() 的妙用

【前端小技巧】你可能不知道的CSS选择器::is(), :where(), :has() 的妙用

专栏开篇: 《前端小技巧集合:让你的代码更优雅高效》
作者: 码力无边


那一夜,我与重复的 CSS 选择器和解了

嘿,各位前端战友们,大家好!我是 [你的名字],一个在代码世界里摸爬滚打,时常因为一个像素的偏移而抓狂,也时常因为一个优雅的实现而沾沾自喜的前端开发者。

今天,是我们《前端小技巧集合》专栏的开篇之作!我寻思着,第一篇文章必须得“镇得住场子”。聊什么呢?聊那些我们天天在写,却可能从未真正“玩明白”的东西——CSS 选择器

回想一下,你是否也曾写下过这样的代码?

/* 令人窒息的重复感扑面而来 */
header h1,
header h2,
header h3,
header h4,
main h1,
main h2,
main h3,
main h4,
footer h1,
footer h2,
footer h3,
footer h4 {
  color: #333;
  font-weight: bold;
}

nav a:hover,
footer a:hover,
.sidebar .link-item:hover {
  color: #ff6347; /* 番茄色,程序员的浪漫 */
  text-decoration: underline;
}

每当写下这种代码,我的内心仿佛有千万只草泥马在奔腾。这不仅仅是丑,它是一种“维护性灾难”。想象一下,如果产品经理突然跑过来说:“小张,我们要在文章详情页 .article-content 里也加上这个标题样式”,你是不是得深吸一口气,然后颤抖着双手,把 , .article-content h1, .article-content h2, ... 这串长长的“尾巴”给续上?

这种代码,我们称之为“代码的坏味道”。它脆弱、冗余,像一个摇摇欲坠的积木塔。

多年以来,我们前端开发者为了解决这个问题,用上了各种“大力丸”:Sass/Less 的嵌套、BEM 命名法… 它们在一定程度上缓解了疼痛,但终究是“外力辅助”。

而今天,我要向你隆重介绍 CSS 原生的“三剑客”,它们将从根本上改变你编写选择器的方式,让你的 CSS 代码瞬间“鸟枪换炮”,充满现代感和优雅感。它们就是—— :is():where():has()

准备好了吗?让我们一起踏上这场选择器的“认知升级”之旅,彻底告别意大利面条式的 CSS!🚀

一、:is() —— 伪类中的“或”逻辑,代码瘦身大师

首先登场的是 :is() 伪类函数。你可以把它想象成一个选择器“收纳盒”,或者更确切地说,是逻辑学中的 “或”(OR)

1.1 痛点重现:没有 :is() 的世界

我们再看一眼那个经典的“标题样式”问题。我们要给 headermainfooter 三个区域里的所有 h1h4 标题设置相同的样式。

传统的写法,就像我们上面展示的那样,是一个巨大的、由逗号分隔的列表。

/* 传统写法:又长又臭 */
header h1, header h2, header h3, header h4,
main h1, main h2, main h3, main h4,
footer h1, footer h2, footer h3, footer h4 {
  /* ...样式... */
}
1.2 :is() 登场:一招制敌

现在,看看 :is() 是如何施展魔法的:

/* :is() 写法:优雅,实在是太优雅了!*/
:is(header, main, footer) :is(h1, h2, h3, h4) {
  color: #333;
  font-weight: bold;
}

🤯 什么?! 就这么简单?

没错!让我们来分解一下这个“咒语”:

  • :is(header, main, footer):这句话的意思是“匹配 header 元素 main 元素 footer 元素”。
  • :is(h1, h2, h3, h4):这句话的意思是“匹配 h1 元素 h2 元素 h3 元素 h4 元素”。

整个选择器连起来读就是:“匹配在 headermainfooter 内部的,h1h2h3h4 元素。

代码的可读性和可维护性瞬间提升了不止一个档次。现在,如果产品经理再让你加一个 .article-content,你只需要:

/* 维护起来不要太爽 */
:is(header, main, footer, .article-content) :is(h1, h2, h3, h4) {
  /* ... */
}
1.3 :is() 的一个重要“脾气”:特异性(Specificity)

天下没有免费的午餐,:is() 在给我们带来便利的同时,也带来了一个需要特别注意的规则::is() 伪类的特异性,由它参数列表中特异性最高的那个选择器决定。

这是什么意思呢?我们来看个例子。

假设我们有以下 HTML 和 CSS:

<div id="app">
  <p>我是 #app 里的 p 标签</p>
</div>
<div class="content">
  <p>我是 .content 里的 p 标签</p>
</div>
<p class="special-p">我是一个特殊的 p 标签</p>
/* 规则A: 使用 :is() */
:is(#app, .content) p {
  color: blue;
}

/* 规则B: 一个普通的类选择器 */
.special-p {
  color: red;
}

/* 规则C: 一个普通的ID选择器 */
#app p {
  color: green;
}

问题来了:

  1. .content 里的 p 标签是什么颜色?
  2. #app 里的 p 标签是什么颜色?

答案可能和你想象的不一样。

解析:

  • 对于选择器 :is(#app, .content) p,它的参数列表是 #app.content
  • #app 是一个 ID 选择器,特异性是 (1, 0, 0)。
  • .content 是一个类选择器,特异性是 (0, 1, 0)。
  • 根据 :is() 的规则,整个 :is(#app, .content) 的特异性取最高值,也就是 #app 的特异性 (1, 0, 0)。
  • 所以,选择器 :is(#app, .content) p 的总特异性是 (1, 0, 1)。

现在我们来比较:

  • .special-p 的特异性是 (0, 1, 0)。
  • #app p 的特异性是 (1, 0, 1)。

回到问题:

  1. .content 里的 p 标签,同时被 :is(#app, .content) p(特异性 1,0,1)和 .special-p(如果它有这个类的话)匹配。但这里它没有,所以它只被 :is() 匹配,颜色是 蓝色 (blue)
  2. #app 里的 p 标签,同时被 :is(#app, .content) p(特异性 1,0,1)和 #app p(特异性 1,0,1)匹配。由于特异性相同,根据“后来者居上”的原则,#app p 在后面,所以最终颜色是 绿色 (green)

划重点: 当你把一个 ID 选择器和一个类选择器混在同一个 :is() 里时,整个 :is() 都会“沾光”,表现得像一个 ID 选择器一样“霸道”。这在你编写可复用的组件样式时,需要格外小心,因为它可能会无意中覆盖掉你期望的其他样式。

二、:where() —— 零特异性的“好好先生”

如果说 :is() 是一个能力超强但脾气有点大的“霸道总裁”,那么 :where() 就是它的双胞胎兄弟,一个性格温和、从不与人争执的“好好先生”。

:where() 的功能和语法与 :is() 几乎完全一样

/* :where() 的用法和 :is() 一模一样 */
:where(header, main, footer) :where(h1, h2, h3, h4) {
  color: #333;
  font-weight: bold;
}

上面的代码,效果和使用 :is() 完全相同。

“那你为什么要单独讲?这不是脱裤子放屁吗?”——别急,关键的区别来了。

2.1 :where() 的超能力:特异性永远为零!

没错,你没看错。无论你在 :where() 的参数列表里放了什么“王炸”——哪怕是 id、内联样式(虽然不能直接放),:where() 自身的特异性 永远是 (0, 0, 0)

我们用上面那个例子来对比一下:

<div id="app">
  <p>我是 #app 里的 p 标签</p>
</div>
<p class="special-p">我是一个特殊的 p 标签</p>
/* 规则A: 使用 :where() */
:where(#app) p {
  color: blue;
}

/* 规则B: 一个普通的类选择器 */
.special-p {
  color: red;
}

现在,#app 里的 p 标签是什么颜色?special-p 是什么颜色?

解析:

  • 选择器 :where(#app) p,虽然参数里有 #app,但 :where() 的特异性是 0。所以整个选择器的特异性只由 p 决定,为 (0, 0, 1)。
  • 选择器 .special-p 的特异性是 (0, 1, 0)。

结果显而易见:

  • 对于 #app 里的 p,即使我们把它放在 :where 里,它自身的样式规则特异性很低,所以它不会影响到其他规则。
  • 如果一个元素同时被 :where(#app) p.special-p 匹配,那么 .special-p 的样式(红色)会因为特异性更高而生效。
2.2 :is() vs :where():我该用哪个?

这可能是你现在最关心的问题。一张图让你秒懂:

特性:is():where()
功能选择器分组(OR 逻辑)选择器分组(OR 逻辑)
特异性由参数中最高的决定永远为零
使用场景当你希望这组样式强制生效,且不轻易被覆盖时。当你编写基础库、CSS Reset、或者可复用组件的默认样式,希望它们能被用户轻松覆盖时。

举个杀手级应用的例子:现代化的 CSS Reset

传统的 CSS Reset (如 normalize.css) 为了确保样式生效,有时会使用一些带有一定特异性的选择器。而当我们想覆盖这些 Reset 样式时,就可能需要提升我们自己选择器的特异性,很不方便。

现在,有了 :where(),我们可以打造一个“零污染”的 CSS Reset:

/* 一个超级温和,绝不惹事的 CSS Reset */
:where(html, body, div, p, h1, h2, h3, h4, h5, h6) {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:where(a) {
  text-decoration: none;
  color: inherit;
}

:where(ul, ol) {
  list-style: none;
}

这段 Reset 代码,由于所有选择器都被 :where() 包裹,它们的特异性全部为 0!这意味着,你在项目的任何地方,只需要写一个最简单的选择器,比如 p { margin: 1em 0; },就能轻松覆盖掉 Reset 的 margin: 0,再也不用担心特异性打架的问题了!

总结一下:

  • 想让样式更“强硬”,用 :is()
  • 想让样式更“谦虚”,方便覆盖,用 :where()

三、:has() —— 千呼万唤始出来,CSS 的“父选择器”革命!

压轴登场的,是这位重量级嘉宾——:has()。前端社区为了它,已经足足期盼了十几年!它被亲切地称为**“父选择器”**,但它的能力远不止于此。它的出现,从根本上颠覆了 CSS“只能向下选择”的传统认知。

3.1 历史性的难题:如何根据子元素来改变父元素?

想象一个场景:你有一个卡片组件,卡片里可能有一张图片和一段描述。产品经理要求:“如果卡片里有图片,那么整个卡片的布局就变成左右两栏;如果没有图片,就还是上下的默认布局。”

在没有 :has() 的年代,你的解决方案是什么?
99% 的情况是:求助 JavaScript

// JS 的常规操作
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
  const image = card.querySelector('img');
  if (image) {
    card.classList.add('has-image-layout');
  }
});

然后在 CSS 里定义 .has-image-layout 的样式。

这没什么问题,但总感觉不够“纯粹”。我们为了一个简单的样式判断,动用了 JS,增加了DOM操作,增加了维护成本。

3.2 :has() 的降临:CSS 的自我救赎

现在,请屏住呼吸,见证奇迹的时刻:

<div class="card">
  <img src="avatar.jpg" alt="Avatar">
  <h3>卡片标题</h3>
  <p>这是一段描述...</p>
</div>

<div class="card">
  <!-- 这个卡片里没有图片 -->
  <h3>另一个标题</h3>
  <p>这是另一段描述...</p>
</div>
.card {
  /* 默认样式:垂直布局 */
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border: 1px solid #ccc;
  padding: 1rem;
}

/* 魔法来了!*/
.card:has(img) {
  /* 如果卡片内部“拥有”一个img元素 */
  flex-direction: row; /* 布局变为水平 */
  align-items: center;
}

发生了什么?

选择器 .card:has(img) 的意思是:“选择一个 .card 元素,条件是:该元素内部必须包含(拥有)至少一个 img 元素。

无需一行 JS!CSS 自己就完成了这个逻辑判断和样式切换。这就是 :has() 的核心魅力:它允许我们基于后代或后续兄弟元素的存在与否来对一个元素(父元素或前置兄弟元素)应用样式。

3.3 玩转 :has():N个让你拍案叫绝的实战场景

:has() 的能力远不止选择父元素。它是一个通用的“关系选择器”,能玩出各种花样。

场景一:智能表单标签

当输入框为必填 (required) 时,给前面的 label 加上一个星号。

<div class="form-group">
  <label>用户名</label>
  <input type="text" required>
</div>
<div class="form-group">
  <label>邮箱(选填)</label>
  <input type="email">
</div>
/* 当 .form-group 内部有 required 的 input 时... */
.form-group:has(input:required) label::after {
  content: ' *';
  color: red;
}

场景二:区分空容器和有内容的容器

一个 div 如果是空的,给它一个提示;如果里面有 p 标签,就正常显示。

<section>
  <!-- I am empty -->
</section>

<section>
  <p>Hello World!</p>
</section>
section {
  min-height: 100px;
  border: 2px dashed #ccc;
}

/* 选择一个内部没有任何元素的 section */
section:not(:has(*))::before {
  content: '这里是空的哦~';
  color: #999;
}

/* 选择一个内部有 p 元素的 section */
section:has(p) {
  border-style: solid;
  border-color: green;
}

场景三:高级兄弟选择器(颠覆性!)

我们都知道 + (相邻兄弟) 和 ~ (后续兄弟) 选择器,它们只能“向后看”。但有了 :has(),我们就能实现“向前看”!

需求:如果一个 h2 后面紧跟着一个 p,那么就给这个 h2 增加一点下边距。

<h2>标题一</h2>
<!-- 这个h2后面没有p,不应用样式 -->

<h2>标题二</h2>
<p>我是一个段落。</p>
/* 选中一个 h2,条件是:它的下一个兄弟元素是 p */
h2:has(+ p) {
  margin-bottom: 2rem;
}

这在以前是绝对不可能用纯 CSS 实现的!:has() 赋予了我们选择“前一个兄弟”的能力,这对于排版和流式布局的微调来说,是革命性的。

场景四:基于子元素数量的布局切换

一个项目列表,如果项目少于5个,就单列显示;如果多于等于5个,就变成网格布局。

<ul class="item-list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<ul class="item-list">
  <!-- 这里有6个li -->
  <li>Item A</li>
  <li>Item B</li>
  <li>Item C</li>
  <li>Item D</li>
  <li>Item E</li>
  <li>Item F</li>
</ul>
.item-list {
  /* 默认单列 */
  list-style: none;
  padding: 0;
}

/* 选中一个 .item-list,条件是:它拥有第5个 li 作为后代
   nth-last-child(n) 是从后往前数第n个
   li:nth-last-child(5) ~ li 意思是:从后往前数第5个li之后,还有其他的li兄弟
   所以,li:nth-last-child(n+5) ~ li 表示至少有5个li */
.item-list:has(li:nth-child(5)) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}

这个例子稍微复杂,但它完美展示了 :has() 与其他伪类结合的强大威力。

3.4 :has() 的特异性

:is() 类似,:has() 的特异性也需要注意。它的特异性等于 宿主选择器:has()前面的部分)与 :has() 参数中特异性最高的选择器 之和。

例如,.card:has(h3.title) 的特异性是:

  • .card (类选择器): (0, 1, 0)
  • h3.title (元素选择器 + 类选择器): (0, 1, 1)
  • 总特异性 = (0, 1, 0) + (0, 1, 1) = (0, 2, 1)

四、兼容性与性能考量:我们能放心用吗?

谈了这么多神奇的功能,一个现实的问题摆在面前:浏览器支持度如何?

  • :is():where()兼容性非常好! 主流现代浏览器(Chrome, Firefox, Safari, Edge)早已支持。你可以放心大胆地在项目中使用,除非你需要兼容非常古老的浏览器(比如 IE,嗯,让我们向它告别吧)。

  • :has()兼容性正在迅速普及! 截至我写这篇文章时(你可以去 Can I Use… 网站查询最新数据),Chrome、Safari、Edge 已经正式支持。Firefox 在 121 版本也已正式支持。这意味着,对于绝大多数面向现代用户的项目,:has() 已经可以进入你的工具箱了!

如何做优雅降级?

对于 :has(),如果你仍然担心部分用户无法看到效果,可以使用 @supports 查询来进行优雅降级:

/* 现代浏览器样式 */
.card:has(img) {
  flex-direction: row;
}

/* 为不支持 :has() 的浏览器提供回退方案 */
@supports not (selector(:has(img))) {
  /* 这里可以写你的 JS 方案的钩子类,比如 .has-image-layout */
  .has-image-layout {
    flex-direction: row;
  }
}

性能考量

你可能会担心,尤其是 :has(),听起来就需要浏览器做很多计算,会不会很慢?

现代浏览器引擎(如 Blink, WebKit, Gecko)在实现这些新特性时,已经做了大量的优化。对于绝大多数日常场景,使用这些选择器带来的性能开销是微乎其微的,完全不必担心。你从减少 JS 操作、简化 DOM 中获得的性能和维护性收益,远远大于这点微小的计算成本。

当然,如果你在一个有成千上万个 DOM 元素的页面上,写了一个极其复杂的、嵌套多层的 :has() 选择器,并把它应用在频繁触发重绘的动画上,那可能会有性能问题。但这属于极端情况,对于任何 CSS 属性都应避免。

五、总结:一图胜千言

让我们用一个表格来清晰地回顾这“三剑客”的特点:

选择器核心功能特异性 (Specificity)主要用途口诀
:is()选择器分组(OR逻辑)由参数中最高的决定简化复杂的选择器列表,并希望样式有较高优先级。霸道总裁
:where()选择器分组(OR逻辑)永远为零编写基础库、CSS Reset,让样式能被轻松覆盖。好好先生
:has()关系选择器(父/前置兄弟)宿主选择器 + 参数中最高根据子/后代元素状态,改变父/前置元素样式。天眼通

写在最后

CSS 的世界一直在进化。从 Flexbox 和 Grid 布局,到 CSS 变量,再到我们今天讨论的 :is(), :where(), :has(),原生 CSS 正在变得前所未有的强大和富有表现力。

掌握这些现代化的选择器,不仅仅是“炫技”,它关乎我们的核心工作:编写更清晰、更健壮、更易于维护的代码。它能让我们把本该由样式表处理的逻辑,从 JavaScript 中解放出来,实现真正的“关注点分离”。

希望今天的分享,能让你对 CSS 选择器有一个全新的认识。从现在开始,拿起你的键盘,在你下一个个人项目或工作中,勇敢地尝试它们吧! 当你用一行 :has() 替代掉几十行 JavaScript 时,那种成就感,无与伦比。


专栏预告与互动:

感谢你读到这里!这是我们《前端小技巧集合》的第一篇。如果你觉得有收获,别忘了点赞、收藏、加关注,这是我持续更新的最大动力!

下一篇,我们将深入探讨【CSS 布局】中的现代技巧,聊聊 gap, aspect-ratio 这些能极大提升你布局效率的神器,敬请期待!

最后,留一个思考题: 你在实际工作中,还遇到过哪些因为选择器限制而头疼的场景?或者,你已经用 :is(), :where(), :has() 玩出了哪些“骚操作”?欢迎在评论区留言分享,我们一起交流进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码力无边-OEC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值