ARTICLE DETAIL

资讯详情

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

jQuery遍历精讲:children、find、closest与集合筛选

jQuery遍历精讲:children、find、closest与集合筛选 平时写页面好多人把 jQuery 用成了“高级选择器”查个元素、改个样式就算完事。可一旦碰上动态列表、多级菜单、表单联动这种需要频繁在 DOM 树里上下移动、筛选节点、批量处理数据的场景光会$(.class)这类基础获取远远不够。jQuery 的遍历方法才是把选择器结果真正用起来的核心手段——children()、find()、parent()、closest()、filter()、each()、map()这一整套 API才是日常开发里和 DOM 打交道的高频工具。这篇内容我会把 jQuery 遍历方法按“父子、兄弟、集合筛选”三个维度完整过一遍把底层原理与选取逻辑讲透重点解决几个特别容易踩坑的细节怎么准确拿到第一个子元素、怎么根据 name 获取对象、怎么高效地取 HTML 里的 id 元素以及遍历时集合快照和链式调用的隐蔽问题。适合刚接触 jQuery 想系统掌握选择器与 DOM 遍历的新手也适合写了一阵子但总在“用错方法导致 DOM 操作结果不对”的开发者做一次自查。1. 用遍历之前先搞清楚手里拿的到底是什么1.1 jQuery 集合的本质类数组与快照很多人以为$(li)返回的是一个“数组”所以直接$(li)[0]取出来一个玩意儿就往里面塞。这个认知不完整但确实很接近真相。jQuery 对象是一个类数组对象它把匹配到的所有原生 DOM 元素按文档顺序放进一个带length属性的对象里同时把这个对象包装成拥有each、filter、find等一堆方法的 jQuery 实例。举个例子var $items $(ul li); console.log($items.length); // 得到 li 总数 console.log($items[0]); // 原生 DOM 元素不是 jQuery 对象 console.log($items.eq(0)); // 第 1 个 li但包装成了 jQuery 对象 console.log($items.first()); // 也是第 1 个 li同样是 jQuery 对象从这里能延伸出一个很关键的点$items[0]是原生 DOM$items.eq(0)是 jQuery 对象。原生 DOM 能用className、value这些属性jQuery 对象能用.attr()、.css()这些方法两者不能混着调否则老手也会写出$items[0].css(color, red)这种报错代码。更值得留意的是jQuery 集合是一个快照snapshot不是在 DOM 上挂了一个活的引用。你执行完$(li)之后页面后续又新增了一个li这个$items里不会自动包含新节点。反过来如果之后删掉了集合里的某个 DOM那么这个 jQuery 对象里的元素就成了“悬空引用”再操作它不会有任何效果也不会报错很容易让你误以为代码没问题而是数据没更新。理解这一点后面排查动态页面问题会省一大半力气。1.2 按 id、name、属性获取元素时最容易忽略的边界热搜词里有“jquery获取html中的id”和“jquery根据name获取对象”这两个恰恰是最常用也最容易出边界问题的入口。先说 id。最标准的方式当然是$(#username)CSS 选择器语法里#代表 idjQuery 内部直接调用原生getElementById做优化速度非常快所以能用 id 就优先用 id。但要注意两个边界如果页面里出现重复 idHTML 规范不允许但实际页面里偶发存在jQuery 只会返回第一个匹配元素。不要指望它给你返回一个集合并通过eq()取出后面几个。你要是想获取元素的 id 属性本身用$(#username).attr(id)或$(#username).prop(id)这两种写法有细微差别attr()读的是 HTML 属性prop()读的是 DOM 属性。id 这类属性两者基本一致但是遇到checked、disabled这种布尔属性时要尤其小心这也是老生常谈的坑了。再说 name 属性。当你要根据 name 获取对象时标准写法是$([nameusername]); // 属性选择器精确匹配 $(input[nameusername]); // 限定标签类型性能更好 $([name^user]); // 以 user 开头可用于批处理 $([name$name]); // 以 name 结尾 $([name*user]); // 包含 user模糊匹配这里有一个很容易被忽略的点name 在单选按钮和复选框场景中天然就是重复的。一组 radio 的 name 相同此时$([namegender])返回的是多个元素的集合你必须用.each()遍历或者:checked伪类筛选不能直接.val()。很多新人在这里翻车以为同一 name 只有一个元素拿到对象后直接读值结果得到 undefined 甚至报错。// 正确做法拿选中项 $(input[namegender]:checked).val(); // 批处理给同一 name 的表单元素统一加事件 $(document).on(change, input[namegender], function () { console.log(this.value); });从这个角度说所谓“遍历方法”不只是each()这种显式遍历工具选择器本身的匹配顺序、返回集合大小、元素顺序也直接决定了你要不要遍历、怎么遍历。先把集合的形状摸清楚后面所有遍历方法才不会用偏。2. 父子兄弟之间的移动三个方向逐一拆解2.1 向下找children、find 与子选择器的取舍向下遍历是使用频率最高的一类常见场景就是树形菜单、表格行操作、列表渲染。jQuery 对“向下”提供了三个不同粒度的工具。children()只往下找直接子元素不跨层级。比如ul idmenu li一级 ul li二级/li /ul /li /ul$(#menu).children(); // 只会选中那个一级 li不会选中嵌套的二级 li $(#menu).children(li.active); // 可以直接传选择器过滤子元素children()的好处是结果可控、性能好它不会深入递归整个子树在层级很深的页面上能明显减少匹配范围。find()则完全不同它会在当前集合所有元素的后代节点里深度查找只要满足选择器就捞出来不限于直接子元素var $allLis $(#menu).find(li);find()几乎是替代“先children()再递归”这种手动写法的万能钥匙。我个人的习惯是要拿所有符合条件的后代时一律直接find()要严格限制一层、且担心不小心把深层节点也选进来时才用children()。这个取舍背后其实是对“结果集意图”的把握——find()返回的是“子孙中所有匹配项”children()返回的是“子层中的匹配项”两者语义不同换着用经常能规避一类 bug层级变化后选择器没跟着变导致选到多余节点。还有一个偏门但有效的写法直接子选择器#menu li这等价于$(#menu).children(li)的效果。写成选择器时更适合用于样式和一次性查找但如果你已经在操作一个 jQuery 对象再用选择器就得重新$()反而多一次解析。所以代码里已经持有集合时用.children(selector)更自然也更贴近“遍历”这个主题的语义。第一个子元素是个高频需求这里集中说清楚四个容易混的写法写法含义典型场景$(#menu).children().first()当前集合里的第一个子元素拿某个容器的首个子节点$(#menu li:first)匹配结果集中第一个 li拿整个列表第一个 li$(#menu li:first-child)每个父元素下的第一个 li多组列表里各自取首项$(#menu li:eq(0))等价于li:first索引方式需要按索引精确控制时很多人把:first-child当成“第一个子元素”的代名词这是经典的语义混淆。:first是集合层面的第一个:first-child是结构层面的判断——只要某个 li 是它父元素的第一个子节点就会匹配所以$(ul li:first-child)在多组 ul 下会返回多个元素而不是一个。想拿“第一个 ul 里的第一个 li”更稳妥的写法是$(ul).first().find(li).first()层级意图清清楚楚。2.2 向上找parent、parents、closest 三兄弟别用混向上遍历的场景更多集中在事件处理上比如点击一个按钮要找到它所在的整行、所在的卡片、或者某个数据容器。这一组方法表面上都是“找爹”但差异很大。parent()只找直接父元素不继续往上。适合“我在按钮内部要找它外面那层 div”这种精确控制。parents()返回所有祖先元素一直到 html 根节点为止。它能传选择器过滤比如$(#btn).parents(.card)会返回所有 class 含 card 的祖先。问题在于如果页面结构里嵌套了多个.card这个集合可能不止一个元素顺序是从内到外很多新手拿到多个结果后用.addClass()结果外层卡片也被一起改了。closest()是三者里语义最独特、也最推荐在事件场景里使用的。它会从当前元素自身开始沿着祖先链向上找直到遇到匹配选择器的节点就返回每个起点最多返回一个元素。注意它包含了“自身”这一点和parents()明显不同$(document).on(click, .delete-btn, function () { // 点击删除按钮找到所在的整行 tr var $tr $(this).closest(tr); // 如果在 tr 本身上点击closest 也会命中而 parents 会漏掉自身 console.log($tr.length); // 1 });closest()特别适合事件委托场景你监听的是 document但真正关心的是触发元素所在的数据行、卡片、弹窗等容器。不用手动parent().parent()链式往上爬也不用先判断e.target再 get 祖先一个closest搞定。做一个简单对比表方便记忆方法起点包含自身返回范围典型用途parent()直接父节点否至多一个元素精确取上一层parents()直接父节点否所有匹配祖先批量改祖先样式closest()自身是首个匹配祖先事件委托中找容器2.3 左右找siblings、prev、next 与同层筛选横向遍历核心方法有siblings()、prev()、prevAll()、next()、nextAll()。siblings()返回除了自己以外的所有兄弟节点同样支持传选择器过滤。最典型的场景就是 tab 切换$(.tab).on(click, function () { $(this).addClass(active); $(this).siblings().removeClass(active); });这里如果写成$(.tab).removeClass(active)再给当前项加 class实际上也能完成但会扫描全部 tab用siblings()缩小到当前项的兄弟范围语义也更贴合“只动这一组”的意图。prev()和next()只取相邻的一个兄弟prevAll()和nextAll()取同一方向上的所有兄弟。方向性的选择通常用于顺序联动比如表格里勾选一行后只处理它后面几行的状态或者步骤条里点亮当前步前面所有步骤。这里有一个链式细节值得提nextAll()默认返回从下一个开始的所有后续兄弟如果你只想取“中间某几项”可以先.nextAll()再用.filter(:lt(3))截断或者直接.nextUntil(.end)这类“直到某条件为止”的方法。nextUntil()在这些场景特别好用因为它让你不必关心中间到底有多少个节点只关心边界条件。横向方法配合addBack()也常有惊喜。addBack()会把前一个遍历对象合并回当前集合里比如你选中一个.item然后.nextAll(.item)拿到后面所有同类项再.addBack()集合就变成“包括自己以及后面所有同类项”。这在分页高亮、步骤导航、连续区间选择里都是非常实用的组合。3. 集合的加工厂each、filter、map 与选择器组合实战3.1 each批量处理的执行顺序与跳出机制each()是 jQuery 遍历中使用率最高的方法没有之一。它的回调函数接收两个参数第一个是索引第二个是当前元素的原生 DOM$(ul li).each(function (index, element) { console.log(index, element.textContent); });回调内的this也指向当前原生 DOM所以$(this)是重新包装成 jQuery 对象的通用写法。这里有一个性能习惯如果你在回调里只需要用一次核验或改一次样式直接$(this)没问题但如果要在同一项上连续调用五次以上方法建议先var $el $(this);缓存一次避免反复包装。each()支持两种“跳出”return false相当于break终止整个遍历return true相当于continue跳过当前项进入下一项。var firstValid null; $(input[namephone]).each(function () { if ($(this).val() ) { return true; // 跳过空值 } firstValid $(this).val(); return false; // 找到第一个非空就停 });这个机制在表单校验、批量赋值、报表统计里非常实用。注意return false终止的是本次each()不是外层函数。很多人想在each()里直接结束整个事件处理函数写了一个return false发现外层函数还在继续往下执行这是因为each回调的返回值只影响遍历本身不影响外部作用域。想中断外层逻辑需要在回调外设置标志位再判断。3.2 filter、not、is筛选不是遍历但比遍历更优雅filter()在功能上可以替代部分each()手写判断的活但它有个本质区别filter()返回的是一个新的 jQuery 集合它不会遍历后逐个执行你自己的逻辑只是把“符合条件”的元素筛出来。filter()可以传选择器、也可以传函数。函数版用起来最灵活// 选择器版当前激活的 li $(ul li).filter(.active); // 函数版值大于 10 的输入框 $(input).filter(function () { return parseInt($(this).val(), 10) 10; });not()与filter()刚好互补not()剔除符合条件的元素。比如$(div).not(.skip); // 排除所有带 skip 类的 divhas()也有很多人忽略。它的语义是“包含匹配元素的子孙”$(.card).has(.badge); // 哪些卡片内部包含 badge 元素这在过滤卡片、模块、分组容器时特别常用比先find()再判断父级要直观得多。is()则不是筛选它返回布尔值用于判断集合中是否有任意元素符合条件常放在条件分支里if ($(this).is(:checked)) { // 复选框已选中 }一个隐蔽的坑是is()不改变返回值数量它只是“存在性判断”。如果你拿$(this).is(.active)去判断单个元素没问题但如果集合里有多个元素哪怕只有一个满足is()也返回 true。想判断“所有元素都满足”那就不能靠is()得用filter().length 集合总长这种方式或者在each()里数一下不满足的数量。3.3 map 与 get从 DOM 集合快速提取数据数组map()在 jQuery 里经常被冷落但它其实是“从页面结构里批量提取数据”的最高效写法。map()会对集合中每个元素执行回调并把回调的返回值收集成一个新的 jQuery 包装数组ul idtags li前端/li li后端/li li运维/li /ulvar tags $(#tags li).map(function () { return $(this).text(); }).get(); // 结果[前端, 后端, 运维]注意最后一定要调.get()。map()返回的还是 jQuery 对象不是原生数组。get()会把内部类数组转成真正的 JavaScript 数组这样你才能用join()、filter()、includes()这些原生数组方法。对比each()手动 push 的做法var tags []; $(#tags li).each(function () { tags.push($(this).text()); });两种都能达到目的但map()更接近函数式风格少写声明、少写 push尤其是需要“提取多个字段组成对象数组”时特别明显var users $(.user-row).map(function () { return { id: $(this).data(id), name: $(this).find(.name).text() }; }).get();需要提醒的是map()回调里如果不返回值对应位置会被当作 undefined 保留还是直接跳过规范上jQuery 的map()默认会忽略回调返回值为 null 或 undefined 的项这一点比原生数组map()更友好但为了避免团队里别人困惑建议在回调里明确 return 而不是依赖默认忽略。3.4 根据 name 获取对象并批量遍历的完整示例把前面几个知识点串成一个常见的表单场景页面里有多组孤儿复选框名字都叫hobby要根据用户勾选情况实时汇总选中值。input typecheckbox namehobby valueread 阅读 input typecheckbox namehobby valuecode 编码 input typecheckbox namehobby valuegame 游戏$(input[namehobby]).on(change, function () { var selected $(input[namehobby]:checked).map(function () { return this.value; }).get(); $(#result).text(已选择 selected.join(, )); });这里用到了三个核心点根据 name 获取对象$(input[namehobby])、用:checked精准筛选选中项、用map().get()提取值数组。整个过程没有手写 for 循环没有each()里 push代码短但是每一步都踩在遍历方法的正确用法上。4. 实操中一定要避开的隐坑集合特性与链式调用4.1 集合快照导致的“遍历失效”与动态页面前面提到过集合的“快照”特性在实际运维页面时会有非常典型的后果。比如你做一个消息列表页面初始有 10 条li你先var $lis $(.msg-list li);保存了集合之后通过接口又加进来 3 条然后你直接在$lis上做批量操作$lis.each(function () { $(this).addClass(processed); });你发现新加的 3 条根本没有 class因为$lis生成于页面初始时刻它的匹配结果只包含当时的 10 条新增节点不在这个集合里。解决办法是在 DOM 更新后重新执行选择器或者不要缓存这种会动态变化的集合改成操作完 DOM 后再统一查一次function refreshList() { // 渲染新列表... var $lis $(.msg-list li); $lis.each(function () { $(this).addClass(processed); }); }这里我给一个实用建议缓存原则是“缓存不会变化的部分重新查找可能变化的部分”。页面容器本身、固定结构层级可以缓存但数据行的数量、内容、顺序永远不要缓存成集合后用很久否则一定会踩到快照过期的问题。补充一个相关坑如果你对集合里的某个节点做了remove()那么操作完最好立刻结束对这个集合的使用。你已经删掉的元素在集合里依然存在但已经不是 DOM 树中的活跃节点对它们的操作不会引发视觉变化但会在遍历时白白增加一次无效处理。4.2 遍历方法的返回值与链式“断链”jQuery 的链式调用很爽但遍历方法里有些会改变集合内容因此会间接影响后续链条的起点。比如$(ul li).filter(.active).addClass(highlight); // 没问题filter 返回筛选后的集合 $(ul li).each(function () { $(this).addClass(highlight); }).find(a).addClass(link-style); // 有问题第二段代码的问题在于each()返回的还是原来的$(ul li)集合不是每个 li 内部的 a。你对each()的结果再调用find()jQuery 会在原来的 li 集合里找后代 a这确实也能找到所有 li 内部的 a看起来“碰巧”能用。但如果你改成$(ul li).each(function () { $(this).addClass(highlight); }).children().addClass(child-style); // children() 拿到的是所有 li 的直接子元素这就有陷阱了第三个.children()作用于整个 li 集合返回的是所有 li 的直接子元素混合体和你预期的“每项”会产生差异。链式调用一旦涉及遍历类方法你脑子里要时刻清楚当前集合已经变成“什么范围”了。最安全的方式是不要把改动集合的过滤操作和批量操作硬链在一起拆成两步写可读性更好。4.3:first-child和:first的混淆结构判断 vs 集合截取这个问题我前面提过但因为它太容易在日常审查里出现值得单独再敲一遍。先看两段代码$(li:first).css(color, red); // 整个页面上第一个 li $(li:first-child).css(color, red); // 所有父元素下的第一个 li如果页面有 5 个ul每个下面 3 个li那第一行只会红 1 个第二行会红 5 个。视觉差异极其明显但很多人调试时只看到“有两行变红了”根本没想到是这两个伪类的语义不同还以为是别的代码冲突。这个误区很容易蔓延到eq(0)、nth-child(1)等写法里eq(0)是集合截取nth-child(1)是结构判断。当你面对“每个分组里的第一个都要处理”这类需求时用:first-child或:nth-child(1)当你只想要“全局第一个”时用:first或eq(0)。记住这个映射关系比死记 API 文档管用。4.4 表单里的 name 选择器别漏引号别漏 :checked根据 name 获取对象的写法$([namexxx])看起来简单但引号问题很隐蔽。如果在路径式属性值后面接伪类$([nameuserName]:checked) // 有时也能工作但属性值边界不清晰 $([nameuserName]:checked) // 推荐写法当 name 值包含特殊字符、空格或连字符时不带引号的选择器会解析失败或匹配异常。除了统一加引号我还建议在能大体确定标签的情况下带上标签名$(input[nameuserName])。jQuery 底层在选择器解析时能利用标签名做快速过滤性能上比裸属性选择器好一些逻辑上也更安全。另外radio、checkbox 这种同名元素批量获取时一定要养成“获取完整集合 用状态伪类筛选”的思维$(input[namehobby]) // 拿全部 $(input[namehobby]:checked) // 拿选中项 $(input[namehobby]:not(:checked)) // 拿未选中项这是 name 属性与遍历方法结合得最紧密的一组 API高频度仅次于 id 选择器。4.5 需要遍历但别遍历事件委托代替批量绑定批量绑定是each()的最常见误用场景。很多人写出这样的代码$(.edit-btn).each(function () { $(this).on(click, function () { ... }); });这段代码本身能工作但本质上是给每个.edit-btn单独绑定事件。如果这些按钮是动态加载出来的新按钮不会自动有事件必须重新绑定非常容易漏。更好的做法是用事件委托$(document).on(click, .edit-btn, function () { var $btn $(this); // 用 closest 找到当前按钮所在的行 var $row $btn.closest(tr); // 处理 });委托把事件挂到稳定的祖先节点上通过冒泡统一处理省掉了遍历绑定也解决了动态节点事件失效的问题。“遍历方法”不是越多越好能不开each就不开each能委托就不循环绑定这也是我在实际项目里总结的一条核心原则。4.6 性能问题高频操作里的遍历优化jQuery 遍历的性能问题主要出现在超大列表、高频触发如 scroll、resize、input 事件、以及嵌套循环里。有几个实用优化点减少重复查询同一个选择器要查询两次以上先缓存var $list $(#list);链式调用中注意范围$list.find(li)永远比全局$(li)快因为搜索范围被限制在子树内批量操作时先分离再操作如果要对大量节点做样式修改考虑先detach()父容器改完再插回减少重排次数jQuery 集合遍历本身不慢慢的是每次属性修改触发的浏览器渲染能用原生方法的地方不绕弯textContent、className直接操作原生 DOM 在某些场景下比$(this).text()、$(this).addClass()快不少说句实话在普通业务页面里jQuery 遍历性能瓶颈很少出现在 API 本身更多是“遍历范围过大”和“遍历过程中误触发了多次 reflow”。把遍历范围缩小、把修改操作合并比纠结用children()还是find()更有效。5. 一个综合案例把前面所有方法串起来光讲 API 不够我拿一个真实改过的需求来复盘。当时要做的是一个动态生成的商品列表每行有图片、标题、价格、一个“上架/下架”开关按钮需求是点击按钮后当前行状态切换同一分组里其他行全部取消高亮并把所有高亮行对应的商品 id 汇总到顶部展示。结构约等于div idproduct-list div classgroup div classproduct-row active>$(document).on(click, .toggle, function () { var $row $(this).closest(.product-row); // 向上找当前行 var $group $row.parent(); // 当前行的分组容器 $row.toggleClass(active); // 如果当前行进入 active则同一组内的兄弟行去掉 active if ($row.hasClass(active)) { $row.siblings(.product-row).removeClass(active); } // 汇总所有 active 行的商品 id var ids $(.product-row.active).map(function () { return $(this).data(id); }).get(); $(#selected-ids).text(ids.join(, )); });这里把上面讲过的要点几乎全用到了closest()找行、parent()分组、siblings()清理兄弟状态、map().get()提取 id 数组。整套逻辑没有手写循环可读性好也容易维护。如果换成每一行都绑定 click 并在回调里$(this).parent().children(.product-row).not(this)这样去查找代码会臃肿很多而且动态新增的行还得重新绑定。这个案例也侧面说明了一个方法论遍历方法解决的是“怎么在 DOM 树上快速定位目标集合”的问题选择器解决的是“从零开始按条件找”的问题两者不是替代关系而是接力关系。拿到目标集合之前用选择器与遍历定位拿到之后用each/map加工数据事件场景里用委托减少直接遍历绑定——这套组合拳打熟了大部分 DOM 操作需求都能在五行之内写出来而且不容易踩坑。我在实际开发中最大的体会是很多人写 jQuery 觉得“玄学”多半不是 API 记不住而是没有分清集合从哪里来、到哪里去。filter()返回新集合不会改原集合find()会把当前集合替换成后代匹配集each()只是过一遍结果但集合本身可能没变化——这些“返回值语义”比方法本身的参数更重要。写代码前先问自己一句“我现在拿到的这个集合到底是什么范围、什么内容”再选方法几乎可以消灭一半以上的遍历 bug。如果再过两三年还在维护旧项目这套能力依然是硬通货即使转向原生 JavaScript理解了 DOM 集合的类数组特性之后再用querySelectorAll、Array.from、filter这些原生 API 也会顺畅得多。遍历这件事本质上就是“在树结构里高效、准确地移动”jQuery 只是帮你把移动的姿势封装得更容易记忆而已。
返回列表