写jQuery代码时,链式调用几乎是最吸引人的特性。选中一组元素,连续调用方法完成操作,一行代码就能完成传统写法好几行的事情。但链越写越长,问题也随之而来:比如先用$('#menu')选中菜单,再用find('li')找到所有列表项,操作完之后还想给菜单容器本身加个class,怎么办?重新写一遍$('#menu')当然可以,但jQuery提供了更优雅的方案——end()方法,它可以回退到上一次筛选操作之前的jQuery对象,让链式调用得以延续。

end()方法的基本用法
end()的作用一句话就能说清:结束当前链中最近一次筛选操作,返回上一次筛选之前的jQuery对象。常见的筛选类方法包括find()、filter()、children()、parents()、eq()、slice()等,这些方法都会改变当前匹配的元素集合,同时在内部把操作前的对象压入一个栈中,end()就是从这个栈里弹出上一个状态。
来看一个直观的例子:
$('#menu') // 选中id为menu的元素
.find('li') // 筛选出所有li子元素
.addClass('item') // 给li添加class
.end() // 回退到#menu对象
.addClass('active') // 给#menu本身添加class
.find('.title') // 再次筛选
.css('color', 'red')
.end() // 又回退到#menu
.show();上面这段代码全程没有重复书写$('#menu')选择器,所有操作都在一条链上完成。每次调用end(),匹配集合就回到上一次筛选前的状态。如果链上没有任何筛选操作,调用end()会返回一个空jQuery对象,这一点要注意,盲目调用可能导致后续操作全部失效且不报错,排查起来比较麻烦。
底层原理:prevObject与pushStack机制
jQuery对象内部有一个prevObject属性,指向筛选前的那个jQuery对象,这一系列引用构成了所谓的“选择器栈”。几乎所有筛选方法在返回新对象之前,都会先调用内部的pushStack()函数,它的逻辑大致是:创建一个新的jQuery对象装载数筛后的元素,并把当前对象记录为新对象的prevObject。
简化后的源码逻辑如下:
// jQuery内部简化实现
pushStack: function(elems) {
// 构建新的jQuery对象,元素为筛选结果
var ret = jQuery.merge(this.constructor(), elems);
// 关键一步:把当前对象压入栈,作为前一个状态
ret.prevObject = this;
ret.context = this.context;
return ret;
},
end: function() {
// 直接返回上一个状态,栈空则返回空对象
return this.prevObject || this.constructor();
}也就是说,end()并不是“撤销”了什么操作,它只是简单地返回prevObject引用的旧对象。之前对DOM做的修改(比如已经加上的class)不会回滚,回退的仅仅是“当前操作的元素集合”。理解了这一点,就不会误以为end()具备撤销功能。
连续多次筛选时,栈会不断加深,end()可以连续调用多次逐层回退:
$('ul')
.find('li') // 第一层筛选:所有li
.filter('.on') // 第二层筛选:class含on的li
.css('background', 'yellow')
.end() // 回退到所有li
.end() // 回退到ul
.css('border', '1px solid #ccc');end()与addBack()的区别及实用技巧
和end()经常一起被提到的还有addBack()(旧版本叫andSelf())。二者方向完全不同:end()是丢弃当前筛选结果,回到之前的状态;addBack()是把之前的状态合并进来,新旧两个集合合并成一个再操作。
举个例子对比,假设要对li和它所属的ul同时设置样式:
// 用end()的写法:分两次设置
$('li.on').css('color', 'red').parent('ul').css('border', '1px solid blue');
// 用addBack()的写法:一次性操作两个集合
$('li.on').parent('ul').addBack().css('border', '1px solid blue');实际开发中还有几个建议值得参考。第一,长链中筛选次数最好不超过三层,否则end()的层次关系很难读懂,可以用换行和缩进提升可读性,如前面示例那样每次筛选后缩进一级。第二,调试时可以在控制台直接查看prevObject,比如$('li').filter('.on').prevObject.length能看到筛选前的元素数量,帮助你确认end()会回退到哪一步。第三,如果你的团队已经在逐步迁移到原生JavaScript,需要知道querySelectorAll返回的NodeList并没有这套栈机制,链式回退的思路需要自己封装实现。
总结一下,end()的本质是利用jQuery对象内部的prevObject栈,把筛选操作前的状态保存下来,让链式调用可以随时回退。掌握了它,配合addBack()等关联方法,在处理复杂的DOM遍历和批量操作时,代码会紧凑得多,也能避免反复执行选择器带来的性能损耗。