2.2 层级选择器:沿血络追索 不是每个目标都自带名牌——页面里大量元素既没有 ID 也没有专属类名,它们只是"某个已知元素旁边的那一个"。上一节的刀法靠元素自身特征命中,这一节换一条路:从已知锚点出发,顺着父子兄弟的血络走过去。四种层级走法是 jQuery 选择器里语法最简、最容易写错的部分,本节把它们一次厘清。 本节摘要:层级选择器共四条路线——空格找全部后代、大于号只找直接子代、加号找紧邻的下一个兄弟、波浪号找身后所有同辈兄弟。它们描述的是"元素之间的关系",真正的命中范围永远由锚点和关系共同决定。 四条血络:语法与语义 在标本页上,这四行的命中数分别是 4、4、2、2。
不是每个目标都自带名牌——页面里大量元素既没有 ID 也没有专属类名,它们只是"某个已知元素旁边的那一个"。上一节的刀法靠元素自身特征命中,这一节换一条路:从已知锚点出发,顺着父子兄弟的血络走过去。四种层级走法是 jQuery 选择器里语法最简、最容易写错的部分,本节把它们一次厘清。
本节摘要:层级选择器共四条路线——空格找全部后代、大于号只找直接子代、加号找紧邻的下一个兄弟、波浪号找身后所有同辈兄弟。它们描述的是"元素之间的关系",真正的命中范围永远由锚点和关系共同决定。
$('#ward li'); // 后代:ward 之内所有 li,无论嵌套多深 $('#queue > li'); // 子代:queue 的直接子级 li,孙辈不算 $('.urgent + li'); // 相邻:紧跟在 urgent 后面的那一个 li $('.urgent ~ li'); // 兄弟:urgent 身后所有同辈 li
在标本页上,这四行的命中数分别是 4、4、2、2。看起来"后代"与"子代"结果相同,只因为标本页只有一层嵌套——一旦队列里出现嵌套列表,两者立刻分道扬镳:
<ul id="queue"> <li class="item urgent">张三 · 心悸</li> <li class="item stable">李四 · 扭伤</li> <li class="item stable">王五 · 复诊 <ul> <li class="item">王五的陪护登记</li> <!-- 嵌套了一层 --> </ul> </li> </ul>
此时 $('#queue li') 命中 4 个(陪护登记也算后代),$('#queue > li') 命中 3 个(陪护登记是孙辈)。**写后代空格之前先问自己:我要不要孙辈?**这一问能避开大半误伤。

层级刀真正的威力在于串联。几个实战频率最高的组合:
// 场景一:队列里紧跟在急症后面的那个普通患者——相邻刀 $('.urgent + .item').text('请这位患者先到二号诊室'); // 场景二:清空主队列的所有直接条目——子代刀保证不动嵌套内容 $('#queue > li').removeClass('urgent stable done'); // 场景三:只想要主面板里的标题,别处的 h2 不许动——后代链 $('#ward .panel-title').addClass('active'); // 场景四:按钮的兄弟全部禁用——兄弟刀 $('#topbar .btn ~ button').prop('disabled', true);
读组合时从右往左翻译更顺:$('#queue > li') 是"选出那些父元素恰为 queue 的 li",右侧永远是最终目标,左侧是它的身世证明。
四条层级路线在原生 querySelectorAll 里语法完全一致,语义也一致——它们本来就是 CSS 选择器。真正的分岔在"方言"上:jQuery 在层级之外还有一批自造的过滤写法((':first')、(':eq(2)')、(':parent') 这类),它们不是 CSS 标准,原生查询不认识:
$('#queue li:first'); // jQuery:第一个 li,正常工作 document.querySelectorAll('#queue li:first'); // 报错:非法选择器 document.querySelectorAll('#queue li:first-child'); // 合法:这是 CSS 标准
习惯把 jQuery 选择器整体粘给原生查询的代码,会在这里翻车。规律很好记:冒号后面是英文单词的常见形态且 CSS 里背过的(first-child、nth-of-type、checked),两边通用;冒号后面带括号参数或明显是缩写的(eq、lt、gt、visible),是 jQuery 方言。
| 路线 | 语法 | 行走方向 | 边界规则 |
|---|---|---|---|
| 后代 | A B |
向下穿透所有层 | 只要血缘上在 A 之内,隔多少层都算 |
| 子代 | A > B |
向下只看一层 | 父亲必须恰是 A 本人,祖父是 A 不算 |
| 相邻 | A + B |
向后看一位 | 只认紧邻的那一个,中间隔着注释以外的节点就失配 |
| 兄弟 | A ~ B |
向后看全部 | 身后所有同辈 B,身前的不算 |
表里最反直觉的是相邻刀的"紧邻"二字:A + B 要求 B 紧跟在 A 之后,若两者之间插进一个换行渲染的文本节点无妨,但插进任何别的标签就再也不匹配。所以给"下一个"这类需求写选择器前,最好先确认中间没有别的元素——不然一改版式选择器就静默失明。
背景:标本页的侧栏原本结构是侧栏、面板、标题三层,样式脚本用 $('#sidebar > .panel > h3') 批量加了下划线。改版时设计师在面板外面套了一层卡片容器,页面一上线,下划线全没了。
操作排查:先数命中——console.log($('#sidebar > .panel > h3').length) 打出零,说明不是样式被覆盖,是根本没选中。逐层验证:$('#sidebar > .panel') 命中为零,而 $('#sidebar .panel') 命中正常。结论:面板不再是侧栏的直接子代,两层子代刀断了一环。
修复与解读:这类需求的稳定写法是"锚点用 ID 保命,中间层用后代空格放宽"——改成 $('#sidebar h3') 或至少 $('#sidebar .panel h3'),把对中间层级的依赖降到最低。层级链写得越深,对文档结构的要求越苛刻,结构微调一次就全线失明;层级选择器的工程原则是够用即可,宁浅勿深。变式:如果标题在页面别处也会出现、不能放宽到整个侧栏,就给面板一个稳定类名做锚,用 .panel-title 这类语义类替代结构位置——能用语义命名的目标,就别用位置关系硬算。
first、eq 这类 jQuery 方言不能喂给原生查询。能沿关系走了还不够快,下一节给刀装上漏斗——在已有集合里按位置、内容、可见性二次筛人。