【前端小技巧】你可能不知道的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()
的世界
我们再看一眼那个经典的“标题样式”问题。我们要给 header
、main
、footer
三个区域里的所有 h1
到 h4
标题设置相同的样式。
传统的写法,就像我们上面展示的那样,是一个巨大的、由逗号分隔的列表。
/* 传统写法:又长又臭 */
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
元素”。
整个选择器连起来读就是:“匹配在 header
或 main
或 footer
内部的,h1
或 h2
或 h3
或 h4
元素。”
代码的可读性和可维护性瞬间提升了不止一个档次。现在,如果产品经理再让你加一个 .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;
}
问题来了:
.content
里的p
标签是什么颜色?#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)。
回到问题:
.content
里的p
标签,同时被:is(#app, .content) p
(特异性 1,0,1)和.special-p
(如果它有这个类的话)匹配。但这里它没有,所以它只被:is()
匹配,颜色是 蓝色 (blue)。#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()
玩出了哪些“骚操作”?欢迎在评论区留言分享,我们一起交流进步!