前端开发中has()选择器突然不宽容了,怎么破?

2,143字
9–14 分钟
in

CSS的:has()选择器刚出来那会儿被捧成“父选择器救星”,结果半路上杀出个程咬金——原本说好的“宽容”人设突然翻车了。以前写:has(h2, ul, ::-scoobydoo)这种带一堆乱七八糟选择器的代码,浏览器会直接忽略那个无效的::-scoobydoo,老老实实匹配h2ul。但现在不行了,只要参数列表里有一个不合法,整条规则直接作废。这波操作让不少老铁写的样式突然失灵,那怎么在不改太多代码的前提下继续愉快玩耍?往下看。

目录

宽容变不宽容

:has()最早在2022年5月的草案里确实是个“老好人”,参数列表里随便混进什么野鸡选择器都不怕,比如article:has(h2, ul, ::-scoobydoo),浏览器会把::-scoobydoo当空气,只认h2ul。但后来W3C收到一个issue,说这种宽容行为和jQuery里的:has()实现打架,尤其是碰到复杂选择器像header h2 + p的时候。于是几周前官方拍板::has()改成“不宽容”模式。现在再写上面那个例子,整个选择器直接废掉,样式压根不生效。好比安检以前看到可疑物品会放行其他东西,现在只要包里有一个打火机,整包行李都给扔出去。

变通方案实操

想要保住原来的宽容效果,得拉两个好兄弟来救场——:is():where()。这俩货到现在还是宽容体质,把它们塞进:has()肚子里就能曲线救国。

方案一:套娃:where()
写代码的时候把原本可能翻车的选择器列表整个丢进:where()里。比如想匹配包含h2ul或者某个瞎写的::-scoobydooarticle,这么干:

article:has(:where(h2, ul, ::-scoobydoo)) {
  background: #f0f0f0;
}

浏览器执行时会先看:where()内部:::-scoobydoo无效?直接忽略,剩下h2ul正常干活。:has()发现:where()返回的结果里有真东西,就认为匹配成功。整个过程像把违禁品塞进一个“豁免袋”,安检只查袋子存不存在,不管袋子里有啥破烂。

方案二:套娃:is()
:is()也是宽容的,但有个小脾气——它的特异性(specificity)会取参数里最狠的那个选择器。同样套法:

article:has(:is(h2, ul, ::-scoobydoo)) {
  border: 1px solid red;
}

无效选择器同样被吃掉,剩下h2ul正常生效。不过要注意,如果h2ul本身特异性一样,那:is()整体特异性按单个标签选择器算(0,0,1),但要是列表里混进个.class,那整坨:is()就变成(0,1,0)

特异性那点事

用哪个方案得看后续样式冲突的情况。:where()是个“零特异性”的佛系青年,它本身不带任何权重,所以嵌套进:has()之后,整个选择器的特异性只由:has()自己贡献。比如:

/* 特异性 (0,0,1) */
article:has(:where(h2, ul, ::-scoobydoo)) {
  color: blue;
}

:is()是个“刺头”,它的特异性等于参数里特异性最高的那个选择器。假设列表里有个#id,那:is()直接飙到(1,0,0),导致整条规则权重爆炸:

/* 特异性 (0,1,0) 假设列表里有个.class */
article:has(:is(.title, ::-scoobydoo)) {
  color: red;
}
方案特异性来源适用场景
:where嵌套只有:has本身需要低权重、易覆盖
:is嵌套取参数最高者必须压过某些样式

实操时如果拿不准,优先用:where()兜底。比如老项目里到处是!important,那可能得靠:is()来硬刚。另外注意:has()本身也会贡献一层特异性(0,0,1),所以:has(:where(...))总特异性就是(0,0,1),而:has(:is(h2, .class))如果.class(0,1,0),总特异性就变成(0,1,1)。写样式前最好用浏览器开发者工具瞄一眼计算后的权重,省得被覆盖得莫名其妙。

代码迁移小抄

假设原来有一堆这种写法:

/* 旧代码 - 现在全废了 */
.card:has(img, video, ::-moz-focus-inner) {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

改成下面任意一种就行。如果不在乎特异性,直接包:where()

.card:has(:where(img, video, ::-moz-focus-inner)) {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

如果后续有更高优先级的样式要覆盖,或者想保留原来那堆选择器里某个高特异性(比如有个#banner),那就包:is()

.card:has(:is(img, video, #banner, ::-moz-focus-inner)) {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

改完记得跑一下测试页面,尤其是那些依赖:has()做交互反馈的组件(比如手风琴、高亮父容器)。万一发现样式没生效,八成是:has()里还藏着别的非法选择器没被包进去——把整个参数列表全塞进:where():is()里最保险。