1. 开始之前:jQuery选择器的定位与学习路径
1.1 为什么现在学jQuery选择器还不过时
“新项目谁还在用jQuery”这个话题,我身边每隔一段时间就会被拿出来吵一次。尤其这两年前端框架越来越重,动不动就是脚手架、组件化,初学者很容易产生一种错觉:jQuery已经是老古董,没必要浪费时间。但只要你碰过一次老项目的维护,或者在兼容要求比较高的活动页里写过脚本,就会理解“jQuery还有必要学吗”这个问题不能一概而论。而即便抛开维护场景,jQuery 选择器本身也值得花时间吃透,因为它是理解“如何精准定位页面元素”的最直接入口。
先摆一个事实:很多仍在稳定运行的后台管理系统、电商平台活动页、旧版 CMS,内部还是大量 jQuery 代码。就算你用 Vue 或 React 重写了大部分功能,只要还保留着 Datatables、EasyUI、日期选择器这类老牌插件,就免不了跟 jQuery 对象打交道。这时候懂不懂选择器,直接决定你能不能在三分钟内改完一个需求,还是连排查都要翻半天文档。
更重要的是,jQuery 选择器的语法几乎沿用了 CSS 选择器规范,又额外扩展了:eq、:contains、:first这类方便伪类。你把这套规则学扎实,后面切换到原生querySelectorAll,或者去理解 CSS 选择器的优先级,都非常顺滑。可以说,选择器是 jQuery 里最值得背下来的一块知识,性价比极高。
1.2 一条适合新手的学习路径和优先级
有些人学 jQuery 喜欢一上来就翻 API 文档,看事件、看动画、看 Ajax,结果用到选择器的时候总是临时查,写出来的代码又长又绕。我的建议是把它当成一条完整链路的前置环节:选择器 → DOM 遍历与操作 → 事件 → 动画 → Ajax → 组件封装。
选择器这一步,又可以按下面的分类顺序去消化:
| 阶段 | 学习内容 | 典型写法 | 解决的问题 |
|---|---|---|---|
| 基础选择器 | id、class、标签、通配、并集 | $("#id")$(".class")$("p") | 快速定位一个或一类元素 |
| 层级选择器 | 后代、子元素、兄弟 | $("ul li")$("ul > li")$("h3 + p") | 按 DOM 结构关系定位元素 |
| 筛选选择器 | 位置、子元素位置、内容、属性 | $("li:first")$("li:first-child") | 在集合内进一步过滤目标 |
这三类掌握以后,再去看.find()、.children()、.filter()这类遍历方法,就会觉得顺理成章。你甚至可以这样理解:选择器负责“找到起点”,遍历方法负责“在起点周围进一步移动”,两者经常搭配使用。
下面我按这三个分类逐个讲,并把实际项目里容易踩的坑标出来。
2. 基础选择器:写 jQuery 的本能反应
2.1 ID 选择器与类选择器
ID 选择器是 jQuery 里查询效率最高的,因为底层可以直接映射到document.getElementById。写法就是$("#id"),#后面对应元素的 id 属性值。类选择器用.class,最终拿到的是一组元素,哪怕页面里只有一个元素匹配,外层也是一个类数组结构的 jQuery 对象。
// 选中 id 为 userName 的元素 var $name = $("#userName"); // 选中所有 class 包含 menu-item 的元素 var $menuItems = $(".menu-item");这里有个新手经常犯的错:拿到类选择器结果以后直接调.value取输入框内容。jQuery 对象不是原生 DOM,取输入框值要用.val(),取文本用.text(),取属性用.attr()。如果确实拿到的是原生 DOM,应该写$(".menu-item")[0].value,或者说用.get(0)。
还要注意,id 在页面中必须是唯一的。后端模板引擎、服务端控件渲染时偶尔会生成带冒号的 id,比如ctl00_ContentPlaceHolder1_txtName这种倒还好,但如果 id 里真的包含:,直接$("#id:name")会被解析成伪类,必须转义成$("#id\\:name")。顺带提一句,ASP.NET GridView 这类控件经常会动态生成复杂 id,所以很多同学习惯用$("input[type=text]")这种属性选择器去规避特殊字符问题。
两个小提醒:
- 同一个选择器如果会被多次使用,先缓存成变量,比如
var $box = $("#box");,避免每次重复查询。 - ID 前面不要画蛇添足写标签名,比如
$("div#box")能跑但没必要,还多一次过滤。
2.2 标签选择器与通配选择器
标签选择器直接写元素名,例如$("div")、$("li")、$("input"),底层借助getElementsByTagName或者querySelectorAll来实现。当你想为某种标签统一加事件、统一加样式时非常省事。
// 所有 tr 加一个 class $("tr").addClass("data-row"); // 所有文本框先清空 $("input[type='text']").val("");标签选择器的问题是匹配范围大,尤其页面里 div 和 span 到处都是,如果只是要给某个区域内的标签加样式,一定要限定上下文。比如$("#main li")远好于$("li")。
通配选择器*匹配的是所有元素,威力大但代价也大。$("*")会遍历整棵 DOM 树,性能损耗明显。实际开发中几乎不会单独用,偶尔会出现在组合选择里,比如$("#box *")选中某个容器里的全部子子孙孙,但能不用就尽量不用。
2.3 并集选择器与基础组合写法
并集选择器用逗号把多个选择器串起来,表示“匹配其中任意一个”。这个规则从 CSS 到 jQuery 都一致。
// 同时选中所有 h2 和 .title 以及 #footer $("h2, .title, #footer").css("border", "1px solid #eee");要注意,返回结果的顺序并不是按照你写选择器的顺序,而是按照元素在文档中出现的顺序排列。这个细节在做索引过滤时很重要,如果你:eq(0)拿到的不是自己以为的那个选择器里的第一个,先想想元素顺序。
并集选择器最常见的场景是多个入口做同样的事,比如“关闭按钮”和“弹窗遮罩”都要触发关闭逻辑:
$(".btn-close, .modal-mask").on("click", function () { $("#modal").hide(); });基础组合中还有一个容易被忽略的写法:上下文参数。$("li", "#list")等价于$("#list").find("li")。这类写法在插件源码里很常见,因为它可以让你在不额外缓存父元素的情况下,快速把范围锁到某个容器内部。
3. 层级选择器:用结构关系定位元素
3.1 后代选择器:一个空格代表“任意子孙”
后代选择器用空格表示,作用是选中某个元素内部所有满足条件的子孙节点,不只是儿子,还包括孙子、重孙子。它是实际项目里用得最多的层级选择器,因为大多数时候我们需要的是容器内部的某类元素,而不是精确到第几层。
<div id="box"> <p>直接段落</p> <section> <p>嵌套段落</p> </section> </div>// 会选择两个 p 元素,包括嵌套在 section 里面的那个 $("#box p").css("color", "red");这个“包含所有子孙”的特性既是优点也是坑。比如一个商品列表页,你在ul上写了$("ul a")去绑点击,但如果a又有自己的子元素图标,点击事件照样会触发;再比如你要统计某个容器内直接有几个子节点,用后代选择器就会多算嵌套层级,这时候就该换用子元素选择器。
3.2 子元素选择器:只认亲儿子
子元素选择器用>连接,只匹配直接子元素,不会再往下钻。它的语法和 CSS 完全一致,很多场景下能帮你避免误伤深层节点。
还是上面那段 HTML:
// 只选中 section 外面的那个 p $("#box > p").css("color", "red");在这里,被选中的只有 div 的直接子元素 p,嵌套在 section 里的 p 会被忽略。这在设计下拉菜单、标签页这类层级分明的组件时特别重要。
我自己的经验是:能用>的地方就别用空格。一方面是为了减少匹配范围、提升一点性能,更重要的是语义更清楚——你写$("#box > p")时,读代码的人一眼就知道操作目标是直接子节点;写$("#box p")时,还得去猜你是不是真的想照顾到深层元素。
顺带说一个性能上的贴心理解:后代选择器需要往树的深处持续扫描,子元素选择器只需检查一层父子关系,所以同样的结构下,>通常比空格更轻快。
3.3 相邻兄弟与后续兄弟:+ 和 ~
兄弟选择器主要处理“同一父元素下的同级元素之间的关系”。相邻兄弟选择器用+,它只会选中紧跟在指定元素后面的那个兄弟元素;通用兄弟选择器用~,会选中指定元素后面的所有兄弟元素。
<h3>文章标题</h3> <p>段落A</p> <p>段落B</p>$("h3 + p").text("只匹配段落A"); $("h3 ~ p").text("匹配段落A和段落B");开发中比较典型的用法是 hover 某个按钮时,显示或激活紧随其后的面板。比如:
$(".btn-open + .dropdown").addClass("show");这种写法比给每个按钮都绑定一个带索引的 id 干净得多。配合事件委托时,兄弟选择器也能减少不少临时变量。
需要注意,+和~都只向后匹配,不会往前找。所以如果你需要“前面的兄弟元素”,jQuery 里没有对应的选择器,得用.prev()方法。
4. 筛选选择器:按位置、状态和内容找元素
4.1 位置筛选:first、last、eq、gt、lt
位置筛选选择器是 jQuery 相对原生 CSS 选择器扩展得比较多的部分,它的作用是站在“匹配结果集合”的角度继续过滤。
:first匹配集合中的第一个元素:last匹配集合中的最后一个元素:eq(index)匹配集合中索引为 index 的元素,索引从 0 开始:gt(index)匹配集合中索引大于 index 的元素:lt(index)匹配集合中索引小于 index 的元素:even匹配索引为偶数的元素,注意这里的偶数索引从 0 开始:odd匹配索引为奇数的元素
$("li:first").addClass("current"); // 第一个 li $("li:eq(2)").text("第三个 li"); // 索引 2 对应第三个 $("li:gt(1)").hide(); // 索引 1 之后的 li 全部隐藏 $("li:lt(2)").hide(); // 索引 0 和 1 的 li 隐藏这几个选择器在做表格分页、轮播、列表高亮时非常顺手。比如要给表格加斑马纹,除了用 CSS 的nth-child(even),在旧项目里也可以这样写:
$("#tableBody tr:even").addClass("even-row");这里有个特别容易踩的坑::even和:odd是以当前匹配集合的索引来计算的,不是以子元素在父容器里的顺序计算的。假设你先把选择器范围缩小到了$(".list li:visible"),那么“第偶数个可见元素”和“DOM 里的第偶数个 li”就是完全两回事。
4.2 子元素位置筛选:first-child 与 nth-child 的区别
上一节的:first是“匹配集合中的第一个”,而:first-child强调的是“父元素的第一个子节点”。这两个名字就差一个词,含义差得很远。
看一段例子:
<ul class="list"> <li>A</li> <li>B</li> </ul> <ul class="list"> <li>C</li> </ul>$("ul.list li:first").length; // 1,取所有 li 里的第一个 $("ul.list li:first-child").length; // 2,A 和 C 都是各自父元素的第一个子节点热词里经常看到“jquery 第一个子元素”,大家搜的大概率是first-child而不是:first。当你希望“每个分组里的第一个”都被选中时,用:first-child;当你只想要“所有匹配结果里的第一个”时,用:first。
同理,:last-child、:nth-child(n)也都是基于父元素的子节点位置来计算的。
// 每个表格行的第一个单元格加粗 $("tr td:first-child").addClass("strong"); // 选中每个列表的第二个子节点(注意 nth-child 从 1 开始) $("ul li:nth-child(2)").css("color", "#c00");:nth-child还支持odd、even以及公式写法,例如:nth-child(2n+1),这套语法和 CSS 规范性一致。用它的好处是天然跟元素在结构中的位置绑定,即便前面同样有多个容器也不会串位。
4.3 内容与状态筛选:contains、empty、visible、checked
除了位置,jQuery 还能按内容、显示状态和表单状态来筛选元素。
// 按文本内容过滤 $("p:contains('报错')").addClass("error-text"); // 空元素,没有子节点也没有文本 $("div:empty").hide(); // 包含某个子元素的父元素 $(".card:has(.btn-danger)").addClass("warning-card"); // 可见不可见 $("#tips:hidden").fadeIn(); $("input:visible").focus();:contains非常实用,但有两个限制。一是参数里如果文本含有引号,匹配会容易出错,需要仔细转义;二是它执行的是子字符串匹配,不是精确匹配,所以:contains('媒体')也会匹配“新媒体运营”。想要精确匹配文本,建议还是用原生遍历或写个过滤器函数。
:visible和:hidden的判断逻辑也不算直观。jQuery 判断隐藏元素,大致依据是 display 为 none、type 为 hidden、宽高为 0,或者父元素已经隐藏。visibility: hidden在多数版本里也会被认定为隐藏,但opacity: 0的元素不算隐藏。写自动化脚本时要特别留意,不要以为透明元素就是隐藏。
表单类状态选择器也很常用:
$("input:checked").val(); $("option:selected").text(); $("input:disabled").prop("disabled", false);4.4 属性筛选选择器:精确匹配你的钩子
属性选择器是筛选选择器里的一组重要分支,它不靠位置、不靠文本,而是看元素的属性特征。常见写法有五种:
[name]带有指定属性名即可[name="value"]属性值精确等于 value[name^="value"]属性值以 value 开头[name$="value"]属性值以 value 结尾[name*="value"]属性值包含 value
// 找所有以 user 开头的 input $("input[name^='user']").css("border", "1px solid red"); // 找所有 pdf 链接 $("a[href$='.pdf']").addClass("pdf-icon"); // 找所有带>$("#productTable").on("mouseenter", "td", function () { var fullText = $(this).text().trim(); // 超过 20 个字符才给 title,避免无意义提示 if (fullText.length > 20) { $(this).attr("title", fullText); } });这里的"#productTable td"是后代选择器,配合事件委托,即使后面的数据是 Ajax 动态加载进来的,也能正常触发。这个场景典型体现了选择器的组合价值:先定位表格,再通过事件委托聚焦到单元格。
5.2 案例:Tab 切换联动内容区
写 Tab 组件时,新手常常写出大量eq(i)配合循环的代码,其实选择器加链式方法就可以做得很干净。
$(".tab > li").on("click", function () { var index = $(this).index(); $(this) .addClass("active") .siblings() .removeClass("active"); $(".tab-content") .eq(index) .addClass("active") .siblings() .removeClass("active"); });这里的$(".tab > li")用子元素选择器,避免内部嵌套结构影响点击目标;.index()方法拿到当前 li 在兄弟节点中的索引;内容区用.eq(index)配合索引选中对应块。整个过程没有写复杂循环,读起来一目了然。
5.3 选择器的性能注意点与最佳实践
选择器性能是一个老生常谈但又容易被忽视的点。以下几点是我在实际项目里反复验证过的:
- 优先用 ID 选择器定位容器,再用
.find()往下找,比写超长后代选择器更清晰。 - 多次使用的查询结果一定缓存成变量,不要每次用到都重新
$()。 - 少用通配选择器,也少用
:hidden、:visible这类全页面范围内过滤的伪类。 - 非要复杂过滤时,可以用
.filter()方法配合函数,可读性通常比堆一长串伪类更好。
// 不推荐:长选择器写一堆 $("div#wrapper .content ul li:visible:first"); // 推荐:分步定位 var $visibleItems = $("#wrapper .content ul li").filter(":visible"); $visibleItems.eq(0).addClass("current");jQuery 底层用的是 Sizzle 选择器引擎,原生querySelectorAll能支持的选择器它会优先交给浏览器原生解析,而那些原生不支持的扩展伪类,比如:eq、:contains,它就会退回到 JavaScript 遍历过滤。所以不是所有选择器都一样省钱,越贴近 CSS 标准的选择器性能越稳定。
6. 常见问题与排查心得
6.1 选择器找不到元素,先检查这几件事
我在帮同事排查问题时发现,选择器“没效果”的原因通常不在选择器语法本身,而在执行时机和对象类型。
第一,代码是不是在 DOM 加载前就执行了。如果脚本写在head里,直接$("#box")拿到的必然为空。解决方案是把代码包在$(function () {})里面,让页面 DOM 就绪后再执行。
第二,是不是把 jQuery 对象和原生 DOM 混用了。$("#id")返回的是 jQuery 对象,你要在控制台打印看看结构,而不是直接打印$("#id").value。判断一个元素到底存不存在,标准写法是$("#id").length > 0。
第三,选择器里的大小写和空格是否准确。class 名和 id 名是区分大小写的,HTML 标签名虽然不区分,但选择器里的属性值区分。还有空格表示后代,如果你写的选择器是$(".menu :first"),中间这个空格会让:first变成所有后代元素中的第一个,往往不是你要的菜单自身。
6.2 动态加载的内容为什么绑定不上事件
这个坑十个新手九个踩。你用一个 id 选择器找到按钮,然后.on("click")绑定事件,按钮一开始不存在,而是 Ajax 返回后才插入页面,那事件自然绑不上。
解决方案是事件委托,把事件绑定到父容器上,再通过选择器过滤触发目标:
$("#tableBody").on("click", "tr[data-id]", function () { // 即使 tr 是后来动态生成的,也能触发 });选择器在这里承担的责任是“过滤触发源”,不是“直接绑定对象”。只要父容器在页面初始时就存在,后续动态插入的元素都能被正确识别。
6.3 我的三个经验技巧
工具再简单,也经不起天天用,用久了自然会有一些自己的习惯。这三个经验是我使用 jQuery 比较高频的技巧:
一是能用data属性钩子就别硬扣 class。比如行选中、状态切换这类需求,优先给元素加>$("ul.nav") .find("li.active") .addClass("current") .end() .addClass("has-active");
三是在调试阶段多利用console.log($("#xxx").length)来验证选择器命中了几个元素,而不是盯着页面看效果。页面效果没出现的时候,往往是选择器没匹配上,不是 jQuery 每个方法都失效了。
7. 最后分享一个我自己的使用习惯
文章写到这里,内容基本已经讲完,最后想分享的是我在实际开发中的一个判断标准:能用简单选择器解决的问题,绝对不用复杂的扩展伪类;能用一次查询加链式方法完成的,绝不写第二遍。选择器只是工具,写得清晰才能真正加快开发速度。
如果新项目里还在纠结要不要用 jQuery,我的看法是,先看你的浏览器支持目标再说。但如果你只是想快速理解 DOM 与 CSS 选择器之间的关系,jQuery 选择器依然是一本很直观的“教科书”。把基础选择器、层级选择器、筛选选择器这三块吃透,后面的路会顺很多。