ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

jQuery选择器九类核心用法与实战避坑指南

jQuery选择器九类核心用法与实战避坑指南 1. 为什么选择器是 jQuery 的立身之本以及 9 类选择器的全景图如果说 jQuery 还有哪样东西是今天依然值得花时间搞懂的我第一个投票给 jQuery 选择器。很多年前我在维护一个“年龄比我工龄还大”的后台系统时被一段长得离谱的原生 DOM 查找代码折磨到怀疑人生。换用 jQuery 之后前后端交互的复杂度一下子降下来靠的就是这 9 类核心选择器。说白了选择器就是一套“按条件捞 DOM 节点”的语法。你在页面上想操作哪个元素、哪一批元素根本不用写一堆getElementById、getElementsByClassName再手动拼接循环一行选择器字符串就能把目标精准地“筛”出来。这套东西能解决的问题往大了说有三类一是跨浏览器兼容早期 IE6/7/8 的年代原生 API 差别大到让人崩溃Sizzle 引擎jQuery 选择器的底层实现帮你把差异抹平了二是组合筛选能力你可以用类似自然语言的条件组合来定位元素比如“所有没有禁用的文本框”就是$(input[typetext]:not(:disabled))第三方库做不到这么简洁三是链式操作的基础选择器选出来的是一组 jQuery 对象后续所有操作都在这条链上展开。这套能力适合谁学如果你刚入门前端先掌握 CSS 基础选择器再来看 jQuery 选择器会非常顺畅如果你在维护老项目、写后台管理系统、做自动化脚本或者准备面试聊 jQuery 底层原理这篇都能直接拿来当参考。很多初学者会问都 2025 年了还要学 jQuery我的看法是工具可以退流行但“怎么高效地筛选一组元素”这个思路不会过时原生querySelectorAll的写法就是从这套思路里长出来的。下面先把 9 类选择器的整体脉络铺开接下来每一类我都会用代码和踩坑经验来说明。类别代表写法筛选维度基本选择器$(#id)、$(.class)、$(div)标签、类名、ID层级选择器$(ul li)、$(ul li)元素之间的结构关系属性选择器$([data-type])、$([href^https])元素的属性名或属性值基础过滤选择器$(li:first)、$(li:eq(2))集合中的位置、状态内容过滤选择器$(li:contains(重点))、$(div:has(span))文本内容、子元素情况可见性过滤选择器$(div:hidden)、$(div:visible)元素显示状态子元素过滤选择器$(li:first-child)、$(li:nth-child(2n))在父元素中的排行表单选择器$(:input)、$(:checkbox)表单控件类型表单对象属性过滤选择器$(input:enabled)、$(option:selected)表单控件的可用、选中状态这 9 类可以分成两个梯队。第一梯队是“找得到”基本、层级、属性这三类解决的是“元素在文档结构里怎么定位”的问题它们对应 CSS 选择器的核心规则第二梯队是“筛得准”六类过滤选择器负责在已经选出来的集合上做精加工比如按索引切片、按内容过滤、按钮状态筛选。实际工作中第一梯队负责缩小范围第二梯队负责精准命中两者经常叠加使用。2. 打基础的三类选择器基本、层级、属性这一节先讲最常用的三类。它们不推荐炫技但必须记忆牢固因为所有复杂的组合写法都是这三类堆出来的。2.1 基本选择器ID、类名、标签与通配符基本选择器只有四个#id、.class、element和*。它们的规则和 CSS 一模一样jQuery 只是换了个$()的壳。看一组实际的例子$(#userName).val(张三); $(.item).css(color, #333); $(tr).addClass(row-hover); $(*).css(margin, 0); // 不太建议全选性能差其中最容易踩的坑有三个。第一个是ID 选择器的唯一性假设。HTML 规范里 ID 应当唯一但如果页面里出现了两个相同 ID尤其是老系统里后端拼接页面时容易犯$(#id)只返回第一个静默丢元素排查起来非常隐蔽。第二个是class 选择器与多类名。选取同时拥有两个类的元素要连着写$(.box.important)中间没有空格。有空格反而变成了层级选择器含义完全变了这点新手经常搞混。第三个是通配符带来的性能问题。$(*)会遍历页面所有节点偶尔在初始化脚本里做全局样式处理可以接受放在高频事件回调里就是灾难。我自己的习惯是能用标签名定位就尽量用标签名能加 ID 限定就加限定。因为 Sizzle 引擎内部对#id有getElementById这个最快路径对标签选择器有getElementsByTagName原生产品。类选择器在旧浏览器里需要遍历全部节点成本明显更高。一个典型的组合写法是$(div#main .content p)先用 ID 缩小范围再往下找性能比全文档找.content好得多。2.2 层级选择器找爸爸、找儿子、找邻居层级选择器是实际操作中最常用的一组它们描述的是元素之间的“亲属关系”。一共四种空格代表后代所有子孙、代表子代直接子元素、代表相邻兄弟紧挨着的下一个、~代表后续兄弟后面所有同级的。// 后代ul 下面所有 li包括嵌套在子 ul 里的 $(ul li); // 子代只选 ul 直接子元素中的 li $(ul li); // 相邻兄弟h2 后面紧挨着的第一个 p $(h2 p); // 后续兄弟h2 后面所有同级 p $(h2 ~ p);我见过不少人把和空格混用。举个具体例子结构是ullispantext/span/liliemtag/em/li/ul$(ul span)能选到 span但$(ul span)什么都选不到因为 span 不是 ul 的直接子元素。这个差异用一句话记空格是“只要在肚子里都算”是“只认亲生的”。和~的区别也容易忽略。$(h2 p)只匹配紧跟在 h2 后面的那一个 p中间隔了其他元素就不匹配$(h2 ~ p)会匹配 h2 之后的所有同级 p不管中间有没有 div、ul 之类的元素挡路。有一回我在一个文章列表页想给每篇摘要加分隔线写的是$(.post .post)结果中间插入广告位元素后样式全乱原因就是要求严格相邻。后来改成.post ~ .post才稳定。使用层级选择器还要注意一个看似别扭但很实用的细节嵌套后代选择器时右侧的匹配范围是左侧范围内的所有子孙节点。这和大白话理解的“找 ul 里的 li”一致但如果你想要的是“每个 ul 下第一层 li 里的第一个子元素”就要写成$(ul li:first-child)少一个都不对。2.3 属性选择器按“出身”筛选元素属性选择器是针对元素标签上那串attrvalue的过滤规则。它很适用于没有现成 class 或 id 可用的场景。语法上延续 CSS 属性选择器的一套七种写法[attr]、[attrvalue]、[attr!value]、[attr^value]、[attr$value]、[attr*value]、[attr|value]。其中[attr!value]是 jQuery 独有的CSS 里没有含义是“没有这个属性或属性值不等于指定值”。// 所有带有>// 第一个 li $(li:first); // 最后一个 li $(li:last); // 索引为 2 的 li即第三个 $(li:eq(2)); // 索引大于 1 的 li第三个及之后 $(li:gt(1)); // 索引小于 3 的 li前三个 $(li:lt(3)); // 排除 class 为 skip 的 li $(li:not(.skip)); // 索引为偶数第1、3、5个注意索引从0开始 $(li:even);最容易翻车的是:first与:first-child的区别。当时我在做一个 tabs 切换需求是“点亮第一个 tab”我写的是$(.tab:first)结果它只在同一个父元素结构正常时看起来正确。后来需求改成“点亮每个选项卡组里的第一个 tab”再用:first就只点了全页面第一个。原因很清晰$(.tab:first)是在“已经匹配到的 .tab 集合”里取第一个全页面鸡犬升天只选一个而$(.tab:first-child)是“每个匹配元素如果它是其父元素的第一个子元素就保留”相当于对多个父容器分别生效。$(#table tr:eq(3))也是一个高频坑点。这里的eq(3)是对整个#table tr集合按文档顺序编号如果表头里有th也要占用索引位置。通常取数据行要写成$(#table tbody tr:eq(3))否则你以为是第四行实际可能是第三行加表头。3.2 内容过滤选择器按文本和子节点过滤内容过滤选择器有四个:contains(text)、:has(selector)、:empty、:parent。:contains匹配“元素文本包含指定字符串”的节点。比如$(p:contains(报错))就能把所有包含“报错”两个字的段落找出来。这个选择器有两个隐藏细节。第一是匹配的是元素的所有文本内容包括子孙元素的文本不是只匹配直接文本节点所以一个 div 里嵌套很深的小字也会被算进去。第二是大小写敏感:contains(Error)匹配不到 “error”。当年我用它做了个前端日志高亮忽略了大小写问题排查了好一阵。// 文本包含“已支付”的 span $(span:contains(已支付)); // 包含 p 子元素的所有 div $(div:has(p)); // 没有任何子节点包括文本节点的元素 $(li:empty); // 有子节点包括文本节点的元素 $(li:parent);:has算是 jQuery 最有价值的强化选择器之一它等于“按后代条件反查祖先”。比如“找出所有包含.error子元素的表单”$(form:has(.error))。要注意:has内部会再做一次选择器匹配所以写得越复杂性能越差。当集合很大的时候更高效的做法是$(form).filter(function() { return $(this).find(.error).length 0; })功能相同但遍历过程更可控。empty和:parent这两个是互补的。$(td:empty)可以快速找出表格中的空单元格方便统一加占位符比如显示“--”。但注意“空”的定义非常严格不含元素也不含任何文本节点。一个写入了空格的td /td就不算空因为空格属于文本节点。我处理表格数据时经常遇到这个情况解决办法是先做一遍$(td).text(function(_, t) { return t.trim() ? -- : t; })再查空。3.3 可见性过滤选择器显隐判断里的那些弯弯绕:hidden和:visible这两个选择器看起来人畜无害用起来全是细节。jQuery 对:hidden的判定标准是display 为 none、type 为 hidden 的 form 元素、宽高为 0、或者祖先元素中任一层的 display 为 none。注意了这里没有提visibility: hidden和opacity: 0。换句话说元素如果是visibility: hidden或opacity: 0在 jQuery 的:hidden眼里它仍然是“可见的”。这跟一般人理解的“看不见就是隐藏”完全不同。// 所有隐藏的 div $(div:hidden); // 所有可见的文本框 $(input:text:visible); // 判断元素是否可见推荐用 jQuery 官方文档的写法 $(elem).is(:visible) // true 表示可见那怎么判断visibility: hidden呢没有专用选择器直接看 CSS 计算样式$(elem).css(visibility) hidden。还有一类元素容易踩坑宽高为 0 的容器。某些占位元素、隐藏 input、就连script标签在 jQuery 里都可能被认为是:hidden。所以统计页面“可见区域”时别直接$(body *:visible)你很可能把脚本标签也算进去。规范做法是限定在 body 的直接子节点范围并且排除 title、script 等标签。另外有个性能陷阱:visible在匹配时需要逐层检查祖先的样式如果页面层级很深、数量又多反复调用会让渲染有明显卡顿。我有一次在 resize 事件里对几百个节点循环is(:visible)页面拖动窗口时明显掉帧。后来改成事件触发时缓存一次结果大幅好转。3.4 子元素过滤选择器第几个孩子不是第几个命中子元素过滤选择器看的是“元素在其父元素中的排行”而不是当前筛选集合里的排名。这一类和前面基础过滤中的:eq特别像但语义完全不同。包含:first-child、:last-child、:nth-child(n)、:nth-last-child(n)、:only-child以及对应的:nth-of-type(n)系列。// 每个 ul 下的第一个 li $(ul li:first-child); // 每个 ul 下的最后一个 li $(ul li:last-child); // 每个 ul 下的第 2 个 li注意从 1 开始数 $(ul li:nth-child(2)); // 每个 ul 下倒数第 1 个 li $(ul li:nth-last-child(1)); // 只有唯一一个 li 子元素的 ul $(ul li:only-child); // 每个 ul 下第 2 个 li 标签 $(ul li:nth-of-type(2));关于nth-child的编号方式网上一搜“jquery 第一个子元素”就有大量人把:nth-child(0)写成“选第 0 个”结果选出来永远是空。:nth-child是从 1 开始计数的:nth-child(1)才是第一个子元素。而:eq是从 0 开始。有意思的是:nth-child(0)也不会报错只是默默什么也不匹配。:nth-child(2n)、:nth-child(odd)、:nth-child(even)这类奇偶写法在表格隔行变色里最常用。但要小心“奇数行”和“索引奇数”的差异:nth-child(odd)匹配第 1、3、5 个孩子视觉上的第一行、第三行而:eq(odd)匹配索引为奇数的元素第 2、4、6 个完全相反。两个都叫“odd”一个是“排行”一个是“下标”我亲眼见过开发把表格斑马纹颜色搞反就是因为没分清楚。nth-of-type和:nth-child的差异也在具体页面里影响很大。如果一个 ul 下同时有 li 和 span 混排li:nth-child(2)要求 li 必须是父元素的第二个孩子而li:nth-of-type(2)只在“li 这种标签”里数排行它是父元素下的第二个 li不管中间穿插了多少其他标签。这个区别在做动态表单字段拆分时特别好用只要记住一句话:nth-of-type只数同标签名的兄弟。4. 表单场景离不开的专用选择器Web 项目里表单永远是最繁琐的部分。jQuery 专门为表单控件定义了两种类型选择器一类按“控件类型”选一类按“控件状态”选。这组选择器在写后台管理系统的查询、筛选、校验逻辑时简直就是救命稻草。4.1 表单类型选择器一个:input覆盖全部控件表单类型选择器最大的好处是不用记一堆input[typetext]、input[typepassword]这种长尾巴比如:text、:password、:radio、:checkbox、:submit、:image、:reset、:button、:file、:hidden以及最常用的:input。// 页面里的所有表单控制元素input/select/textarea/button $(:input); // 所有文本框包括 typetext 和某些老浏览器里的默认类型 $(input:text); // 所有复选框 $(:checkbox); // 所有单选按钮 $(:radio); // 所有下拉框注意没有 :select 这个简写要直接用标签名 $(select); // 所有文件上传 $(input:file);:input是一个很特别的聚合选择器它一次性覆盖 input、select、textarea、button 四种标签。做“表单一键清空”时$(:input, form) 或$(#myForm :input)配合.not(:button, :submit, :reset, :image)就能把所有可输入的控件一次性揪出来。要注意:input 也包含按钮类控件用的时候脑子里得有这个范围别把提交按钮给清空了。还有一个容易忽略的注意点$(input:text)在某些场景下会把没有明确type属性的 input 也匹配进来因为浏览器默认把它当文本框处理。如果你页面里还有typesearch、typeemail、typenumber这类新型输入框:text是不会匹配它们的需要单独处理。做“一键清空所有输入类控件”时我会额外写$(input[typeemail], input[typenumber], input[typesearch], input:not([type]))来补全。4.2 表单状态选择器选中、可用、勾选状态选择器是针对表单控件的当前交互状态进行过滤:enabled、:disabled、:checked、:selected。// 所有可用输入框 $(input:enabled); // 所有禁用输入框 $(input:disabled); // 所有被勾选的复选框和单选按钮 $(:checked); // 所有被选中的下拉选项 $(option:selected);实际工程里:checked和:selected的配合非常常见。比如做一个“批量删除选中项”的功能$(#deleteBtn).on(click, function() { var ids $(.user-row).filter(function() { return $(this).find(input[typecheckbox]).is(:checked); }).map(function() { return $(this).data(id); }).get(); // 这里把 id 数组交给后端 });:disabled的判定有一个隐含规则父级 fieldset 禁用时表单控件会被浏览器视为 disabled。jQuery 的:disabled同样受这个规则影响。如果你的表单里用了 fieldset 做分区个别分区的控件被整体禁用单查input:disabled依然能匹配到它们。反过来你想用.prop(disabled, false)解禁这些元素时只改单个控件是不起作用的得先把 fieldset 的 disabled 属性移除。这个坑我踩过一次排查了半天最后发现是祖先 fieldset 在作怪。:selected用得稍少但处理联动下拉时很实用获取当前选中项的值$(#province option:selected).val()。注意这里是option:selected而不是select:selected因为选中的状态发生在 option 元素上。5. 三个真实场景实战拆解这一部分把前面讲过的知识放进具体场景里。我拿三个高频热搜词当作素材每一个都是实际开发中反复遇到的需求。5.1 场景一取第一个子元素——:first-child、:first、eq(0)到底用哪个搜索引擎里 “jquery 第一个子元素” 的热度一直很高。根源在于三个写法长得太像语义却完全不同。// 写法 A在整个匹配结果集合里取第一个 $(ul li:first); // 写法 B每个 ul 下第一个 li本质是“属于父元素第一个子节点的 li” $(ul li:first-child); // 写法 C在整个匹配结果集合里取索引 0 $(ul li).eq(0);假设页面里有三个 ul每个 ul 下有两个 liulliA1/liliA2/li/ul ulliB1/liliB2/li/ul ulliC1/liliC2/li/ul$(ul li:first)只选中 A1。$(ul li:first-child)选中 A1、B1、C1。$(ul li).eq(0)也只选中 A1和写法 A 等价但不依赖选择器语法语义更直白。所以“取第一个子元素”这个需求得先明确是“取第一个匹配元素”还是“每个父元素下的第一个子元素”。我自己的判断顺序是如果只想要第一个直接$(...).first()或$(...).eq(0)可读性最好如果想让每个容器下的第一个子元素都被选中用:first-child不要在复杂结构里用:first因为它很容易被误读成“第一个子元素”实际却是“第一个匹配元素”排查代码的人会被带偏。5.2 场景二DataTables 单元格内容过长鼠标悬浮展示全部数据这个场景来自实际的表格开发关键词很具体“jquery datatable 单元格内容过长展示.. 鼠标悬浮展示全部数据”。后台管理列表里字段太长导致表格被挤爆是最常见的 UI 问题。思路分三层CSS 截断、title 原生提示、自定义浮层。先做 CSS 层td.text-overflow { max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }这样单元格视觉上只会显示省略号但仅仅这样用户看不清完整内容。接着用 jQuery 给元素加 title。DataTables 的 columns 配置里用 render 函数给每一格补上 title 属性$(#dataTable).DataTable({ columns: [ { data: title, render: function(data, type, row) { return div classtext-overflow title (data || ) (data || ) /div; }} ] });用户鼠标悬停后浏览器原生会弹出标题提示不用任何额外插件零成本。但原生 title 提示有两个缺点样式不可控、出现有延迟。如果产品要求更高的悬浮体验需要做自定义浮层。思路是在mouseenter时往 body 里追加一个绝对定位的 divmouseleave时移除$(document).on(mouseenter, .text-overflow, function() { var $this $(this); var fullText $this.attr(data-full) || $this.text(); $(div classtooltip-box fullText /div) .appendTo(body) .css({ top: $this.offset().top - $this.outerHeight() - 8, left: $this.offset().left }); }).on(mouseleave, .text-overflow, function() { $(.tooltip-box).remove(); });注意这里用事件委托而不是直接给每个 td 绑事件因为 DataTables 翻页后 DOM 会重新渲染直接绑定的 handler 会丢失。这个经验适合所有“表格数据动态刷新”的场景。另外浮层要设置pointer-events: none否则用户鼠标从触发元素移到浮层的瞬间两个事件会相互干扰浮层会闪烁消失这类体验问题很难用代码排查最好一开始就给浮层加上这个样式。5.3 场景三在浏览器控制台快速抓取“左上角那个元素”有人搜“左上角的元素选择器”其实问的是调试技巧页面上某个元素肉眼可见却没有 id、没有 class怎么用程序选中它这里给你一个 Chrome DevTools jQuery 的实战组合技。先在控制台里执行// 获取浏览器视口左上角位置考虑到可能有偏移的元素 document.elementFromPoint(10, 10);这个原生 API 会返回视口坐标 (10, 10) 处最上层的元素默认是覆盖在最上面的那个节点。配合 jQuery 选中并操作它// 把视口左上角元素包装成 jQuery 对象并标红 $(document.elementFromPoint(10, 10)).css(outline, 2px solid red);但真实页面里左上角往往是固定的 header、侧边栏等非目标元素。更常用的做法是配合 DevTools 的 Elements 面板先选中元素控制台里直接$0就能拿到这个元素再套一层$($0)转成 jQuery 对象。$0是 DevTools 提供的临时变量记录的是 Elements 面板当前选中的节点这个技巧比手动写选择器快得多。如果目标元素想从一堆 DOM 里按文本定位就用:contains。比如我要抓页面中所有含“取消”二字的按钮// 给包含“取消”文本的按钮加边框 $(button:contains(取消)).css(border, 2px dashed red);这套组合拳基本能应对绝大多数“不知道选择器怎么写”的调试场景。调试时大胆用一次选不中就在祖先元素上继续套条件。6. 常见问题与性能排查实录含速查表前面各章节里已经穿插了不少坑这一节集中总结我在项目中真正遇到过的、以及同行群里高频讨论的问题做成速查表你可以直接收藏。问题错误示范正确做法说明ID 选择器不生效$(#user-name)找不到但 DOM 里确实有检查是否有特殊字符id 含.或:时必须转义$(#user\\.name)class 选择器多选了$(.box .title)想选 .box 自己去掉空格$(.box.title)空格是后代选择器:eq和:nth-child混用$(li:eq(1))以为选的是第二个 ul 下的第二个 li分清集合索引 vs 父元素排行:eq(1)是整个集合第二个:nth-child(1)才是每个父元素的第一个孩子获取选中值拿不到$(select:selected).val()$(select option:selected).val()selected 状态在 option 上不在 select 上动态内容不响应翻页后 click 事件失效使用事件委托$(document).on(click, .btn, fn)直接绑定只作用于当前存在的 DOM特殊字符转义不生效$(#a.b)报错或找不到$(#a\\.b)或$(#a\\.b)注意双反斜杠.#[]等都要\\\\转义全选性能差页面卡顿避免$(*)高频执行通配符选择器不缓存、全遍历属性值为中文$([data-name张三])报错加引号$([data-name张三])中文值在部分浏览器里解析成非法标识符:hidden不生效visibility: hidden的元素用:hidden判断用css(visibility)判断jQuery 的:hidden不含 visibility 和 opacity:contains选不中:contains(data)匹配不到Data先统一转小写再匹配:contains大小写敏感除了这些常见错误还有几个性能经验可以分享。第一个经验缓存 jQuery 对象。不管什么选择器每执行一次$()都要重新遍历 DOM。在一个循环里反复写$(.item)性能会肉眼可见地变差。正确做法var $items $(.item); for (var i 0; i $items.length; i) { // 操作 $items.eq(i) }第二个经验优先缩小范围再精准命中。$(.item)全文档查找$(#container .item)先在#container容器内查找。在没有缓存的情况下后者通常更快。如果容器是动态刷新的局部区域可以在刷新局部时重新获取容器引用。第三个经验:has、:contains这类“内容过滤”选择器是性能洼地。它们必须把目标节点的文本内容或子节点全部拉一遍再匹配当集合超过几千个节点时延迟会很可观。能用:has的地方尽量改成filter函数代码可读性反而更高// 不推荐很慢 var result $(.module:has(.error)); // 推荐性能接近可读性更高 var result $(.module).filter(function() { return $(this).find(.error).length 0; });第四个经验别在选择器字符串里做过深的层级嵌套。$(#table tbody tr td .content a)这种五层以上的写法调试时很难定位问题而且每次匹配都要遍历多个层级。更好的做法是先确定一个最近的、唯一可识别的容器再配合find缩小范围var $tbody $(#table tbody); var links $tbody.find(a.content-link);第五个经验选择器报错不是 jQuery 的问题而是字符串语法问题。遇到Uncaught Error: Syntax error, unrecognized expression时优先检查引号、转义、特殊字符。我一度以为是插件冲突后来发现是某段数据里的括号没有转义导致选择器解析失败。7. 最后分享几条我的个人经验代码写多了以后我对选择器的心得一句话可以概括选择器是 API不是魔法。它只是帮你定位元素的工具真正决定代码质量的是你有没有想清楚“我到底要选哪些元素、这些元素会有什么状态变化”。我个人的写作习惯是页面上明显的模块容器优先用 ID 定位模块内部的元素用 class 定位不要在多处重复写完整的后代路径凡是以后可能被动态替换或追加的元素都走事件委托涉及大量文本匹配的场景先考虑封闭的函数过滤而不是选择器硬刚。还有一个踩过几次坑之后养成的习惯所有从接口或后端拼接来的数据在写进选择器之前一律先做替换处理。比如列表数据里的 id 是user#123直接拼进选择器$(#user#123)会报错得先把#转义成\\#或者干脆不用 id 做选择器改用>
返回列表