:not(:has(*)) 是真正“完全无子”的判断,匹配没有任意子节点(包括文本、注释、元素)的父元素,比 :empty 更严格,但受空格、换行、注释影响,现代浏览器支持,无 polyfill。

父元素空时隐藏,用 :not(:has(*)) 最直接
现代浏览器(Chrome 105+、Firefox 119+、Safari 15.4+)支持 :has(),所以判断“子元素是否存在”这件事,不再需要 JS 或冗余 class。核心就是::not(:has(*)) 匹配**没有任意子节点(包括文本、注释、元素)的父元素**。
注意::has(*) 不等于 :empty —— :empty 会忽略空白文本节点,而 :has(*) 只要存在一个子节点(哪怕只是 或换行符产生的文本节点),就为真。
-
:not(:has(*))是真正“完全无子”的判断,比:empty更严格 - 如果父元素里有空格、换行、注释,
:not(:has(*))就不生效 —— 这是常见踩坑点 - 想兼容旧浏览器?这条路走不通,
:has()无 polyfill,必须降级用 JS 或预设 class
为什么不用 :empty?它和 :not(:has(*)) 的区别在哪
:empty 只看是否「有子元素节点」,会忽略纯空白文本节点;而 :not(:has(*)) 是「一个子节点都不能有」,包括文本、注释、元素。
比如这段 HTML:
立即学习“前端免费学习笔记(深入)”;
<div class="container"> </div>
表面看是空的,但换行符生成了文本节点,:empty 不匹配,:not(:has(*)) 也不匹配 —— 它俩都失败。真正能触发 :not(:has(*)) 的,只有完全无内容的写法:<div class="container"></div>(同一行、无空格)。
- 模板中用
v-if/*ngIf/ JSX 条件渲染时,更容易产出真正“无子”的 DOM,适合配:not(:has(*)) - 服务端渲染或手写 HTML 时,缩进和换行大概率导致失效,得手动清理或改用 JS 检查
childElementCount === 0 && trim(textContent) === '' -
:empty性能略好(原生伪类),但语义不如:not(:has(*))精准 —— 它无法排除<div> </div>这类情况
:not(:has(.item)) 和 :not(:has(*)) 别混用
如果你只关心某类子元素(比如列表项),用 :not(:has(.item));如果目标是「任何子都没有」,就必须用 :not(:has(*))。两者行为完全不同。
示例:
<ul class="list"> <li class="item">A</li> <li>B</li> </ul>
-
.list:not(:has(.item))→ 不匹配(因为有.item) -
.list:not(:has(*))→ 不匹配(因为有<li>元素) -
.list:not(:has(li))→ 匹配(只要没<li>,哪怕有<p>也成立)
参数差异很关键:通配符 * 表示「任意子节点」,不是「任意元素」——它包含文本、注释;而 .item 或 li 只匹配元素节点。
实际项目中容易被忽略的细节
最常被忽略的不是语法,而是 DOM 构建方式本身:
- Vue / React 中用
v-for/map()渲染列表,若数组为空,容器很可能真的变成<ul></ul>—— 这时:not(:has(*))才可靠 - HTML 模板里写
<div class="card">{{ content }}</div>,当content为空字符串,结果是<div class="card"></div>,没问题;但若后端返回" "(空格),就会生成文本节点,:not(:has(*))失效 - CSS 选择器不触发重排,但
:has()在某些复杂嵌套下可能影响性能(尤其深层:has(:has(:has(...)))),单层:not(:has(*))影响可忽略
真正难的不是写对那行 CSS,而是确保 DOM 在各种数据边界下,确实达到「无子」状态。


















