ARTICLE DETAIL

资讯详情

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

jQuery选择器:前端开发者真正该吃透的通用查询思维

jQuery选择器:前端开发者真正该吃透的通用查询思维 先说一个我这两年经常被问到的问题jQuery 还有必要学吗尤其现在大家一开口就是 Vue、React我要是回答“学”好像有点站着说话不腰疼要是回答“别学”可我自己当年靠 jQuery 选择器解决的线上问题今天用原生 JavaScript 照样会遇到。我的真实态度是整个 jQuery 库你可以不用但它的选择器体系你一定要吃透。原因很简单jQuery 选择器的筛选逻辑和 CSS 选择器几乎同源学会它你就同时掌握了原生querySelectorAll的查找思路、DOM 结构的观察方法以及日后不管换什么框架都需要的“精准定位元素”的能力。这篇主要梳理 jQuery 选择器三块内容基础选择器、层级选择器、筛选选择器顺手把学习路径也理一理。文章里我不会只贴语法会把每个选择器适合什么场景、容易在哪翻车说清楚。最后一节还会聊性能和几个经典翻车现场这部分是我自己真实踩过的坑希望你能少走一遍。1. 学习路径为什么选择器比 jQuery 本身更值得学1.1 先说结论库可以不用选择器思想一定要有我发现一个很有趣的现象很多新人学原生 JavaScript 时遇到“获取元素”这个需求第一反应就是document.querySelector或document.querySelectorAll说明大家都意识到选择器的价值了。但一旦条件变复杂比方说“取表单里所有未勾选的 checkbox”很多人又开始写for循环加if判断。根本原因不是他不会写循环而是选择器掌握不够不知道这只是$(input[typecheckbox]:not(:checked))一条表达式的事。选择器思想本质上是一种“结构化查询思维”先观察 HTML 结构再把你想要的元素具象化成一条表达式。这个思维不依赖任何框架。我平时带新人时也常拿一些“看似与 jQuery 无关”的问题来验证比如有同事问过“vue-amazing-ui 日期选择器里月份是英文怎么改成中文”。这类问题看着跟选择器没关系但背后的解决思路完全相通你先理解组件结构、找到配置项在哪个层级再决定是改 prop 还是操作 DOM。如果你有选择器思维会自然地去“定位 修改”没有这个思维就会一股脑钻进源码里去瞎改。工具会换定位问题的思路不会变。1.2 我的推荐路线从 CSS 选择器到原生 API 再到 jQuery如果你完全是个新手不建议直接上来背 jQuery 选择器语法。我建议按下面这条路线走一遍每一步都有明确目的第一步把 HTML 和 CSS 选择器基础弄熟。理解 DOM 树里的父子、兄弟关系理解class、id、伪类这些概念。jQuery 选择器不是凭空发明的它对标的本来就是 CSS 选择器。第二步用原生 API 做一遍同样的事。document.querySelector和querySelectorAll能覆盖日常 80% 的查找需求。用它们练习“从复杂页面里定位元素”的能力。第三步再回来看 jQuery 选择器。这时你会发现语法几乎是无缝迁移的只是 jQuery 返回的是jQuery对象原生的返回的是NodeList或单元素。第四步选一两个真实项目练手把选择器、事件、Ajax 串起来。比如做个学生信息管理页选择器负责取到目标 DOM事件负责交互Ajax 负责提交数据。很多人是被“jQuery API 太多”劝退的。但你不需要背全部 API先把选择器这一段吃透后面的事件、动画、Ajax 都是顺着用的东西。选择器是骨架其他 API 是肉。1.3 别把它当新技术要把它当“手感”有些同学心理上会抗拒学一个“过时的库”觉得学了没用。实际上 jQuery 选择器在现代开发中出现的频率比你想象中高得多维护五年前的老项目、写自动化测试脚本、用爬虫处理动态页面、在控制台调试时快速定位节点这些场景都要用到。我自己现在写原生 JS也经常在控制台里用$()系语法做快速调试。甚至去读 Vue 项目的模板编译产物时看到querySelector的匹配逻辑依然会回到选择器这套规则上来。所以与其纠结“要不要学 jQuery”不如说“要不要掌握结构化查询这门通用语言”。我的判断很明确值得而且越早越好。2. 基础选择器ID、类、标签、通配符的适用边界2.1 ID 选择器是性能天花板但别到处挂 id$(#username)是最直观、最暴力的选择器。它底层映射的是getElementById在 jQuery 内部会被单独加速处理所以单个元素的获取它是性能天花板。但是ID 选择器有一个隐性成本页面里 ID 必须唯一命名即约束。一旦你给页面元素大量挂 ID后面代码维护时就会面临命名冲突、语义混乱、样式优先级被 ID 权重绑架等问题。我见过不少老项目页面里几十个div idaaa最后只能靠正则全局替换改成 class。所以我的建议是一个模块的根节点用 ID 做入口够了根节点内部的元素优先用 class。比如// 根入口用 ID内部定位用类 $(#registerForm .error-msg).css(color, red);这样既保证性能又不会把 ID 当成“低级 class”到处用。2.2 类选择器多类叠加业务状态用法的关键.error-msg、.btn、.active这类类选择器是日常最高频的写法。类选择器最灵活的地方在于可以“多类叠加”.btn.disabled表示“同时拥有 btn 和 disabled 两个类的元素”。它非常适合做业务状态切换比如步骤条、Tab 高亮、表单校验错误提示。实际操作中类名本身也要有语义。我建议把类名当成“状态标记”而不是“样式描述”。比较一下.red是样式描述明后天主题色一换就尴尬.is-error是状态描述无论颜色怎么改这个类名始终表达“这里出错了”。jQuery 选择器和 CSS 无缝配合把状态类用好很多交互逻辑就清晰了// 切换错误状态 $(#registerForm .is-error).removeClass(is-error); $(#username).addClass(is-error);类选择器返回的永远是“所有匹配元素”不是一个。这个认知很重要后面很多坑都从这里来。2.3 标签选择器与通配选择器该省则省标签选择器写法最简单$(div)、$(span)语义也直白选页面里所有该类标签。但正因为太宽泛它往往不能单独用。你写一个$(div)可能把页面里几十上百个 div 全拖进来后续操作就会失控。我通常会把它和其他条件配合使用$(div.form-item)表示“是 div 且带 form-item 类的元素”。这样可以有效避免把无类名的 div 也选中。通配选择器$(*)就更激进了它会匹配所有元素。在 jQuery 里它偶尔会出现在“清空某个容器所有内容”的场景$(#content *).off().remove(); // 避免事件残留再清空但日常开发尽量不要直接写$(*)尤其是把它挂在复杂选择器尾部性能会很难看。基本原则是能缩小范围就缩小范围能加锚点就加锚点。2.4 复合与或关系一次写对组合条件基础选择器最容易被忽略的是“组合逻辑”。上面提到的div.form-item是“同时满足”的复合写法元素 类名。还有一种“或关系”用逗号分隔// 找出 username 和 city 两个输入项一起设为禁用 $(#username, #city).prop(disabled, true);这里有个新手高频错误把“同时满足”写成了“后代关系”。比如你想选“带有 form-item 类并且带有 is-error 类的元素”正确的是.form-item.is-error中间不要加空格。一旦写成.form-item .is-error含义就变成了“form-item 内部的 is-error 后代元素”完全是另一种东西。这类差异等你看完下一节的层级选择器会更敏感。我把基础选择器总结成一张速查表方便你对比选择器写法匹配范围性能特点ID#username单个元素最优走 getElementById类.error-msg所有带该类的元素优一般走类名索引标签div所有该标签元素快但范围太宽通配*所有元素慎用范围不可控复合div.form-item同时满足条件范围被有效收缩或#username, #city任一条件满足分别匹配后合并3. 层级选择器用 DOM 结构关系精准定位3.1 后代选择器与子元素选择器一个空格引发的误伤层级选择器是 jQuery 选择器里最有价值的一部分因为它利用的是 DOM 树天然的结构关系而不是靠人肉遍历。后代选择器$(#nav li)匹配#nav内部所有li无论嵌套多少层。子元素选择器$(#nav li)只匹配#nav的直接子元素li。一个空格就可能造成“误伤”。最经典的例子是嵌套菜单ul idnav li一级菜单 ul li二级菜单/li /ul /li /ul如果我用$(#nav li)选中的是 3 个 li包括二级菜单如果我只想操作一级菜单的 li必须写$(#nav li)结果才只有 2 个。这个差异在多层列表里尤其致命做层级菜单折叠时很多人就是在这里把子节点一起误绑了事件。所以我的习惯是先问自己“我要的是直接孩子还是所有后代”再决定用空格还是。选错方向后续代码再正确也白搭。3.2 相邻兄弟与通用兄弟列表、表单里的常用招式兄弟关系在实际开发里也很常见。表示“紧挨着的下一个兄弟”~表示“后面所有的兄弟”。举个例子表单里经常有这样的结构输入框后面跟一个错误提示 span。当校验出错时我只想操作这个输入框后面的提示而不是页面里所有提示// 相邻兄弟只改 username 后面的提示文本 $(#username .error-msg).text(用户名不能为空);再比如步骤条里选中某一项后需要让它后面的所有步骤项都变成可选状态// 通用兄弟当前项后面所有兄弟 $(.step-item.active ~ .step-item).removeClass(disabled);这里有个细节~只匹配“后面的”所有兄弟不会匹配前面的也不会匹配不在同一个父元素下的元素。所以使用前最好在浏览器里确认一下结构看看目标元素确实在同一层。3.3 GridView 表格场景复杂层级下的定位思路接触过 ASP.NET 的人对GridView应该不陌生它渲染出来的表格结构层级很重外层是 table里面有行、列列里可能还有 label、span、a 标签。想在这么复杂的结构里定位“第 3 行第 2 个单元格里的文本”最省事的就是层级选择器配合位置筛选// 取 GridView 中第 3 行的第 2 个单元格 $(#gridView tr).eq(2).find(td).eq(1).text();或者直接用组合选择器$(#gridView tr:eq(2) td:eq(1)).text();注意索引从 0 开始tr:eq(2)是第 3 行。这也是为什么我鼓励学层级选择器面对自动生成的复杂表格与其在 JS 里一层一层children下去不如先用选择器把路径描述出来。选择器写得好代码行数和心智负担都能降一大截。4. 筛选选择器在结果集上精挑细选4.1 位置类筛选索引从 0 开始很多新人栽在这里筛选选择器的作用对象是“已经选中的集合”它帮你在这个集合里继续挑。位置类筛选是我们最先用到的一批:first第一个元素:last最后一个元素:eq(index)指定索引的元素索引从 0 开始:even索引为偶数的元素:odd索引为奇数的元素:gt(index)索引大于指定值的元素:lt(index)索引小于指定值的元素大多数新人第一次栽跟头就在这以为:eq(1)是“第二个”没问题的但写:even时误以为它会选“第 1、3、5…个”实际上 jQuery 里:even匹配的是索引 0、2、4…也就是视觉上的第 1、3、5…个方向恰好和人的直觉相反。写表格隔行变色时这是高频 bug。举个例子现在有一个表单我想隐藏第二个表单项$(.form-item:eq(1)).hide();这里索引 1 对应的是第二个.form-item。如果你不确定可以在页面里先console.log($(.form-item).length)确认数量再用:eq逐个验证。4.2:first和:first-child到底有多大区别这个区分非常重要尤其对于搜索热词里“jquery 第一个子元素”这类需求来说答案就是这两个选择器的差异$(li:first)所有li元素的集合里选第一个li。$(li:first-child)每个父元素下选择“作为第一个子元素”的li。如果某个父元素的第一个孩子不是li那么这个li不会被选中。一句话总结:first是对集合做过滤:first-child是对结构位置做判断。举个例子页面上有三个ul每个ul下面都有几个li。$(li:first)只会得到页面里第一个li而$(li:first-child)会得到三个li因为三个ul各自都有一个“身为第一个子元素”的li。同理:last和:last-child也是这个区别。使用之前先想想你要的是“全局集合中的第一个”还是“每个父容器下的第一个”。这是完全不同的两个业务场景。4.3 内容与结构筛选:contains、:has、:not、:empty:contains(文本)根据文本内容匹配元素比如找到所有包含“用户名”字样的 label。:has(span.error-msg)匹配包含某个子元素的容器常用于判断某个表单项下面是不是已经有错误提示。:not(选择器)排除指定条件比如$(.form-item:not(.active))。:empty选择没有子元素包括文本节点的空元素。这几个里:contains和:has很容易被误用成“CSS 里也能用”其实它们是 jQuery 扩展出来的伪类浏览器原生 CSS 并不支持所以匹配速度不如标准伪类快。我建议在大列表里谨慎使用:contains毕竟它要检查每个候选元素里的文本内容数据量大时会有明显卡顿。:has是一个挺实用的选择器。比如动态渲染表单时我想找到已经存在错误提示的那一项// 找到内部已经存在 span.error-msg 的 form-item $(.form-item:has(span.error-msg)).addClass(has-error);比先遍历再find要干净不少。4.4 表单筛选:input、:checked、:selected组合拳表单是选择器重灾区因为表单元素类型太多标签名又不一样。input、textarea、select、button手写起来很容易漏。jQuery 提供了一套专门针对表单的筛选选择器:input匹配所有 input、textarea、select、button 表单元素:text匹配 typetext 的输入框:password、:radio、:checkbox、:file、:submit等按 type 匹配:checked匹配被选中的 radio 和 checkbox:selected匹配被选中的 option:disabled、:enabled匹配禁用/可用状态实际使用中README里常见的写法是先限定表单容器再做表单筛选避免影响页面其他区域// 取整个注册表单里所有文本输入框 $(#registerForm :text); // 取已选中的编程语言 checkbox $(#registerForm input[typecheckbox]:checked); // 取下拉框当前选中的项 $(#city option:selected).val();特别注意:input和input的区别。input只是标签选择器匹配的是input标签:input还包括select、textarea、button。如果你要一次性操作整个表单里的所有控件:input是更准确的选择。4.5 实战datatable 单元格过长悬浮展示全部内容标题相关热词里有个日期选择器列表jquery datatable 单元格内容过长、鼠标悬浮展示全部数据。这类需求在表格页非常常见尤其是列很多的数据报表。用 jQuery 选择器配合一点原生 DOM 判断就能实现。思路是拿到表格里所有td逐个检查单元格内容是否溢出。判断标准通常用scrollWidth clientWidth内容的实际宽度超过了可视宽度说明被截断了。然后给这些单元格加一个悬浮效果鼠标移上去时展示完整内容。$(#dataTable tbody td).each(function () { const cell this; // 内容溢出的单元格才需要处理 if (cell.scrollWidth cell.clientWidth) { const fullText $(cell).text(); $(cell) .addClass(cell-overflow) .attr(data-title, fullText) .css(cursor, pointer); } });配合一段简单 CSS把td设置为max-width加text-overflow: ellipsis再用attr(data-title)做原生悬浮提示或者接一个轻量 tooltip 组件就能实现“悬浮展示全部数据”的交互。这里就用到了一整条选择器链路$(#dataTable tbody td)是范围限定 标签筛选.each()是遍历结果集$(cell)把原生 DOM 再包回 jQuery 对象继续操作。如果一开始写的是$(#dataTable td)也有可能会把表头里的 td 一起选中所以我特意加了tbody这算是一个小经验表格操作尽量限定到tbody避免把表头一起拖下水。5. 性能检查与翻车现场选择器快慢和失效的真相5.1 从右往左匹配最右侧选择器决定性能jQuery 内部早期用的是 Sizzle 引擎匹配机制和 CSS 类似大致是“从右往左”找候选元素再逐级向父级验证。也就是说#nav .item这个选择器会先找所有.item再检查它们的祖先里有没有#nav。这带来一个实用结论最右侧的选择器越宽泛性能越差。$(div .item)和$(#nav .item)相比后者第一步就只找.item第二步才核对祖先是不是#nav前者第一步匹配所有.item范围已经很大了。另外:contains、:has、:eq这类筛选器往往无法交给浏览器原生的querySelectorAll处理jQuery 需要自己“捞出来再过滤”所以性能和标准选择器差距明显。我的习惯是能用标准属性选择器或类筛选就不用扩展伪类非用不可时尽量配合:eq、:first把候选集合缩小。5.2 选择器不是实时绑定动态内容要用事件委托这是老项目里最常踩的坑之一。你写$(.item).on(click, fn)时jQuery 会给页面里“当前已存在”的所有.item绑定事件。之后如果你用append、html()动态新增了一个.item它会变得“裸奔”——没有事件。这不是选择器写错了而是选择器本质是一次“快照查询”不负责追踪未来加入的节点。解决方案有两条一是新增节点后重新绑定事件二是用事件委托// 事件委托由 #list 统一接管未来新增 .item 也有效 $(#list).on(click, .item, function () { // 处理逻辑 });事件委托原理简单说就是事件冒泡到#list容器时再判断触发对象是不是.item。这样动态添加的.item不用额外绑定天然生效。这个知识点不算选择器本体但它是“选择器失效”最常见的场景之一值得放在一起说。5.3 那几个气得想拍桌的坑空格、缓存、:hidden先说空格。大多数人都会在某一时刻把复合选择器写错。想选“同时带a和b类”的元素写成.a .b结果页面里啥都选不中或者选中了完全意料之外的嵌套元素。这里务必记住空格代表后代关系不是并列关系。并列用逗号同时满足用连续书写。再说缓存。每次$(#someId)都会执行一次查询如果在循环里反复写等于反复扫描 DOM// 错误示范循环里重复查询 for (let i 0; i list.length; i) { $(#nav).find(li).eq(i).text(list[i]); }正确做法是把结果对象缓存到变量里// 正确只查一次循环里操作缓存对象 const $navItems $(#nav li); for (let i 0; i list.length; i) { $navItems.eq(i).text(list[i]); }最后说:hidden。jQuery 里对:hidden的定义跟很多人直觉不一样它不只针对display: none还包括input typehidden、显式宽高为 0、以及祖先元素不可见等情况。而 CSS 里的visibility: hidden或者opacity: 0元素仍然占据布局空间在 jQuery 的:hidden判定里表现可能和你预期不同。所以当你想选“视觉上不可见但仍在文档流里的元素”时不要直接用:hidden最好用filter或自行判断getComputedStyle。这类细节网上讨论不多但真实业务里很容易踩。5.4 jQuery 对象不是数组取 DOM 元素的正确姿势$(.form-item)返回的不是数组而是一个 jQuery 对象。它长得像数组有length属性也支持索引访问$(.form-item)[0]但本质上是一个包装对象。很多新手拿到 jQuery 对象后直接当数组用调用.map、.forEach结果发现压根不是自己预期的行为。正确做法是遍历用.each()它不是原生forEach但回调里的this指向原生 DOM 元素。按索引取用.eq(index)返回的是 jQuery 对象可以继续链式调用用[index]返回的才是原生 DOM 元素。// jQuery 对象遍历 $(.form-item).each(function () { console.log($(this).text()); // this 是原生 DOM }); // 取第二个元素继续操作 $(.form-item).eq(1).hide();这个知识点直接关系到选择器的“后续操作”。我见过太多人拿到选择器结果后卡在“为什么取出的元素不能直接textContent”这个问题上。理解了“jQuery 对象 vs 原生 DOM”的区别选择器乔接起来就顺畅了。最后分享一个我自己坚持了很多年的习惯凡是能用选择器一次性表达的需求我绝不在 JS 里写循环去“找”元素。代码评审时只要看到有人用for循环在 DOM 里翻来找去我的第一反应就是这不就是一条选择器的事吗选择器本质是一种结构化查询思维它让你从“手工翻抽屉”变成“报出坐标”效率和准确性完全不一样。把 jQuery 选择器练到条件反射再去用querySelectorAll、或者各种框架里的“数据驱动查找”你会发现很多概念都是相通的。选择器不只是语法更是一辈子的手感。这个系列我打算接着写下去后面可以聊聊选择器与事件绑定的配合、属性选择器在真实业务里的用法以及数据表格和树形结构里那些“看起来很难实际一条选择器就搞定”的场景。①这一篇先把地基打牢你在自己项目里把基础、层级、筛选三类选择器各用上几遍再回来看后续内容会更有感觉。
返回列表