有过jQuery使用经验的开发者几乎都写过这样的代码:$('div').addClass('active').siblings().removeClass('active').end().css('color','red');。这一长串操作能够连续执行,并且在end()之后能回到上一个元素集合,靠的就是jQuery内部一个并不对外暴露的方法——pushStack。它每次被调用时并不会在原有对象上直接修改,而是会创建一个全新的jQuery实例,同时保留对旧实例的引用,从而形成一条可以回溯的链式堆栈。

下面我们从源码实现、回溯机制以及常见API的使用三个角度,把这个堆栈的细节彻底讲清楚。
一、pushStack源码拆解:新对象从哪来
要理解pushStack,首先得知道jQuery对象并不是普通的DOM数组。它本质上是一个类数组对象,除了length属性和数字索引对应的DOM元素外,还携带了selector、context、prevObject等上下文信息。这些附加属性正是链式调用能够回溯的关键。如果pushStack直接返回this,那么所有操作都会共享同一个对象,end()也就失去了回退的参照。
打开jQuery源码,pushStack的核心实现大致如下(简化版):
pushStack: function( elems ) {
// 用一个空的jQuery实例承载新元素集合
var ret = jQuery.merge( this.constructor(), elems );
// 记录上一个jQuery对象,形成链式关系
ret.prevObject = this;
// 保留原始上下文,便于后续操作
ret.context = this.context;
return ret;
}
仔细观察可以发现,它首先通过this.constructor()创建了一个全新的jQuery空对象,然后用jQuery.merge把传入的elems合并进去。最关键的一步是ret.prevObject = this,它把调用前的旧对象引用保存到了新对象上。这样一来,每一次链式操作都会产生一个新对象,旧对象则作为“前一个状态”被挂在新对象下面,形成一条单向链表。
这种“新建对象”而非“原地修改”的设计,是jQuery链式调用机制的精髓。它让每次操作都能拥有独立的元素集合和状态,同时又不会丢失历史上下文。例如$('div')选择出来的对象,经过.find('span')后返回的新对象里,prevObject就指向原来的$('div')对象,链式关系得以保留。
二、链式调用的堆栈回溯:end()与prevObject
end()方法的实现极其简单,源码中几乎就是一行:return this.prevObject || this.constructor();。意思很明确:如果当前对象存在prevObject,就返回它;否则返回一个空的jQuery实例。也就是说,end()每次只会沿着prevObject链后退一步,而不是直接跳回起点。
为了直观理解,看下面这段代码:
var $first = $('div.item');
var $second = $first.find('span');
console.log($second.prevObject === $first); // true
var $third = $second.addClass('hl');
console.log($third.prevObject === $second); // true
var $back = $third.end();
console.log($back === $second); // true
在这个例子中,$first是最初的jQuery对象,$second是find('span')返回的新对象,它的prevObject指向$first。接着addClass('hl')又产生$third,其prevObject指向$second。当调用$third.end()时,返回的就是$second,而不是$first。如果要回到最初状态,需要连续调用两次end():$third.end().end()。
这种堆栈式的回溯设计非常灵活。你可以在链式调用中随时退回到任意一层,继续基于旧有的元素集合进行新的操作。另外,addBack()方法也利用了prevObject:它内部会调用this.pushStack( jQuery.merge( this, this.prevObject ) ),将当前集合与上一个集合合并后产生新对象。理解了pushStack,addBack的源码也会变得一目了然。
三、pushStack在常见API中的应用与注意点
jQuery中很多集合操作方法都间接使用了pushStack,比如eq()、slice()、filter()、map()等。它们内部在得到新的元素集合后,都会调用pushStack返回新对象并设置prevObject。因此这些方法不会破坏链式调用的连续性。相反,get()方法返回的是原生DOM元素或数组,调用后jQuery链就此断开,后续无法再使用end()等jQuery方法。
var $items = $('li');
var $firstItem = $items.eq(0);
console.log($firstItem.prevObject === $items); // true
var $filtered = $items.filter('.active');
console.log($filtered.prevObject === $items); // true
var $backToItems = $filtered.end();
console.log($backToItems === $items); // true
上面的代码展示了eq()和filter()返回的新对象都正确保存了prevObject,所以end()能顺利回到原来的$items。需要注意的是,链式调用过长时,随着每次操作都创建新对象并保留旧对象引用,内存中会形成一条较长的对象链。虽然现代JavaScript引擎的垃圾回收机制能处理大部分情况,但在处理大规模DOM集合或频繁链式操作时,还是应当避免无限嵌套的链式写法。
另一个容易踩坑的点是:end()只回退一步,而不是重置到第一个选择器。很多初学者误以为调用一次end()就能回到起点,结果发现经过多次集合变换后,需要逐级回退。明确这一点后,在编写复杂交互逻辑时就可以更精确地控制上下文切换,避免写出难以维护的超长链式代码。总的来说,pushStack是jQuery链式调用的底层基石,吃透它的工作机制,无论是阅读插件源码还是编写自己的jQuery扩展,都会更加得心应手。