CSS的:has()选择器刚出来那会儿被捧成“父选择器救星”,结果半路上杀出个程咬金——原本说好的“宽容”人设突然翻车了。以前写:has(h2, ul, ::-scoobydoo)这种带一堆乱七八糟选择器的代码,浏览器会直接忽略那个无效的::-scoobydoo,老老实实匹配h2和ul。但现在不行了,只要参数列表里有一个不合法,整条规则直接作废。这波操作让不少老铁写的样式突然失灵,那怎么在不改太多代码的前提下继续愉快玩耍?往下看。
宽容变不宽容
:has()最早在2022年5月的草案里确实是个“老好人”,参数列表里随便混进什么野鸡选择器都不怕,比如article:has(h2, ul, ::-scoobydoo),浏览器会把::-scoobydoo当空气,只认h2和ul。但后来W3C收到一个issue,说这种宽容行为和jQuery里的:has()实现打架,尤其是碰到复杂选择器像header h2 + p的时候。于是几周前官方拍板::has()改成“不宽容”模式。现在再写上面那个例子,整个选择器直接废掉,样式压根不生效。好比安检以前看到可疑物品会放行其他东西,现在只要包里有一个打火机,整包行李都给扔出去。
变通方案实操
想要保住原来的宽容效果,得拉两个好兄弟来救场——:is()和:where()。这俩货到现在还是宽容体质,把它们塞进:has()肚子里就能曲线救国。
方案一:套娃:where()
写代码的时候把原本可能翻车的选择器列表整个丢进:where()里。比如想匹配包含h2、ul或者某个瞎写的::-scoobydoo的article,这么干:
article:has(:where(h2, ul, ::-scoobydoo)) {
background: #f0f0f0;
}浏览器执行时会先看:where()内部:::-scoobydoo无效?直接忽略,剩下h2和ul正常干活。:has()发现:where()返回的结果里有真东西,就认为匹配成功。整个过程像把违禁品塞进一个“豁免袋”,安检只查袋子存不存在,不管袋子里有啥破烂。
方案二:套娃:is():is()也是宽容的,但有个小脾气——它的特异性(specificity)会取参数里最狠的那个选择器。同样套法:
article:has(:is(h2, ul, ::-scoobydoo)) {
border: 1px solid red;
}无效选择器同样被吃掉,剩下h2和ul正常生效。不过要注意,如果h2和ul本身特异性一样,那: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()里最保险。
