先记住这个答案
:is() 和 :where() 都接受一组选择器作为参数,匹配其中任意一个就算命中。写法和效果完全一样,唯一的区别是优先级。:is(.a, #b) 的优先级按参数中最高的 #b 来算,达到 ID 级别。而 :where(.a, #b) 的优先级始终是 0,随便一个类选择器就能覆盖。
:is()优先级取参数中最高的:where()优先级永远是 0- 两者都能简化重复的选择器嵌套
:where()适合写易覆盖的基础样式
用它们简化选择器嵌套
以前给多层嵌套的元素写样式,经常要把选择器复制好几遍。比如文章里的标题链接,手动列举特别啰嗦。:is() 可以把重复的部分折叠起来。
/* 以前的写法,每个标题都要列出来 */
.article h1 a,
.article h2 a,
.article h3 a { color: inherit; }
/* 用 :is() 简化 */
.article :is(h1, h2, h3) a { color: inherit; }两种写法选中的元素完全一样。但优先级不同:传统写法里每条的优先级是一个类加一个标签加一个标签,:is() 版本里 :is(h1, h2, h3) 的优先级只取标签级别。
如果参数里混了 ID 选择器,比如 :is(#sidebar, .main),整个 :is() 的优先级就被拉到 ID 级别。哪怕实际命中的是 .main,优先级也按 #sidebar 来算。这一点容易踩坑。
where() 适合写可覆盖的默认样式
做组件库或 CSS reset 时,希望预设的样式尽量容易被业务代码覆盖。:where() 就是为这个场景设计的,因为它的优先级是 0,任何带类名的选择器都能盖过它。
/* reset 样式用 :where(),优先级为 0 */
:where(ul, ol) {
list-style: none;
padding: 0;
margin: 0;
}
/* 业务代码随便一个类就能覆盖 */
.nav-list {
padding: 16px;
}如果 reset 用的是 :is(ul, ol) 或者直接写 ul, ol,优先级虽然只有标签级别,但某些情况下还是会和业务样式打架。用 :where() 就完全没有这个问题。Tailwind CSS 的 preflight 就大量使用 :where(),保证用户写的任何工具类都能覆盖 reset 里的样式。
容错解析:一个坏选择器不连累整组
传统选择器列表 h1, h2::xyz, h3 {} 里如果 h2::xyz 不合法,浏览器会把整条规则丢弃,h1 和 h3 也不会生效。
:is() 和 :where() 改进了这一点。:is(h1, h2::xyz, h3) 里 h2::xyz 不合法时,只跳过它,h1 和 h3 照常匹配。这在跨浏览器写样式时很有用,可以把实验性选择器和标准选择器放在一起,旧浏览器自动忽略不认识的那个。
容易答错的地方
- 以为 is() 的优先级按实际命中的参数算
- 不是。
:is(.a, #b)无论实际命中的是.a还是#b,优先级都按最高的#b来算。这是规范明确规定的,取参数列表中优先级最高的那个。 - 把 where() 当成 is() 的别名用
- 功能上确实一样,但优先级天差地别。
:where()的优先级是 0,意味着几乎所有正常写法都能覆盖它。在需要样式有一定强度的场景用了:where(),很容易被意外覆盖。
面试官还会怎么问?
is() 能嵌套使用吗?
可以。:is(.card, .panel) :is(h1, h2) 完全合法,匹配 .card 或 .panel 里面的 h1 或 h2。嵌套使用时优先级各自独立计算,外层取外层参数的最高值,内层取内层参数的最高值。
能不能用 is() 匹配伪元素?
不能。:is(::before, ::after) 是不合法的。规范要求 :is() 和 :where() 的参数只能是复合选择器,不能是伪元素。想给多个伪元素写相同样式,还是要分开写。
CSS 原生嵌套和 is() 有什么关系?
CSS 原生嵌套规范里,浏览器在某些情况下会自动把父选择器包裹在 :is() 里。比如 .a { .b & {} } 解析后相当于 .b :is(.a)。所以使用原生嵌套时要注意,:is() 的优先级规则会悄悄参与计算。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。