jQuery 链式调用

链式调用是 jQuery 的设计精髓——每个方法都返回 jQuery 对象本身,让你能像流水线一样把一连串操作串成一行。这种风格在当年极具颠覆性,影响了之后无数库(包括 lodash、moment.js)。理解链式调用,就读懂了 jQuery 的"灵魂"。

1. 链式调用的原理

链式调用能成立的唯一前提:每个方法都 return this(或返回一个新的 jQuery 对象)。这样你调完一个方法,拿到的还是 jQuery 对象,可以继续调下一个。

// 链式调用:几乎每个 jQuery 方法都返回 jQuery 对象本身
// 所以可以一路 .下去
$("#title").text("新标题").css("color", "red").addClass("big");

// 上面等价于:
const $t = $("#title");
$t.text("新标题");
$t.css("color", "red");
$t.addClass("big");

// 经典:把一段操作压成一行
$("button")
  .text("点我")
  .addClass("btn-primary")
  .on("click", function () { alert("hi"); })
  .appendTo("body");

// 动画也能链(自动排队)
$("#box")
  .fadeOut(500)
  .delay(1000)
  .fadeIn(500);

从读代码的角度,链式调用本质是"对选中的这批元素依次做几件事"。把它当成"流水线"或"管道",从上往下读就是执行顺序。动画方法fadeOut 等)也会自动排队,所以多段动画连写就是序列动画。

2. .end():回溯选区

链式调用里有个不太直观但极有用的方法:.end()。它会"撤销"上一次的选区切换,回到上一轮选中的元素。

// .end():回溯到上一轮选区
// 场景:选中 #list,先操作它,再操作它的子元素

// 不用 end:要存两个变量
const $list = $("#list");
$list.addClass("active");
$list.find("li").addClass("item");

// 用 end:一条链搞定
$("#list")
  .addClass("active")            // 对 #list 操作
  .find("li")                    // 切换选区到 li
  .addClass("item")              // 对所有 li 操作
  .end()                         // 回溯到 #list
  .css("background", "#eee");    // 继续对 #list 操作

// jQuery 内部用一个"选区栈"维护每次切换
// find / children / filter / add / slice 等会"入栈"
// end() 把栈顶弹出,回到前一个选区

原理是 jQuery 内部维护一个"选区栈":每次调用 find / children / filter 等遍历方法,新的选区会"入栈";end() 则"出栈"回到前一个。这个机制让你在不存中间变量的情况下,灵活切换操作目标。

3. .addBack():合并选区

有时候你不希望"切换"选区,而是"合并"——既操作当前选区,也保留前一轮。.addBack() 就是干这个的。

// .addBack():把当前选区和上一轮选区合并
// jQuery 1.8 之前叫 .andSelf(),已废弃

// 场景:给 .item 及其容器都加 class
$(".item")
  .parent()
  .addClass("highlight");         // 只给 parent 加,item 自己没加

$(".item")
  .parent()
  .addBack()                      // 同时选 parent 和 .item
  .addClass("highlight");          // 都加

// .add():把新元素加进当前选区
$("h1").add("h2").add("h3").addClass("title");
// 等价 $("h1, h2, h3").addClass("title");

// 链式栈操作总结
//  入栈(切换选区): find / children / filter / parents / closest / siblings / add
//  出栈(回到上轮): end / addBack

还有 .add() 用来往选区里加新元素——你可以在链中临时把别的元素拉进来一起操作。记住这套栈操作的口诀:遍历方法入栈,end 出栈,addBack 合并。

4. 实战:链式调用的真实场景

把选择器、事件、动画、链式调用串起来,看几个典型写法——这是十年前 jQuery 项目的"标准范式"。

// 实战:用链式调用做一个 dropdown
$(".dropdown")
  .on("mouseenter", function () {
    $(this)
      .addClass("open")
      .children(".menu")
      .stop(true)
      .slideDown(200);
  })
  .on("mouseleave", function () {
    $(this)
      .removeClass("open")
      .children(".menu")
      .stop(true)
      .slideUp(200);
  });

// 实战:点击列表项,标记为已选,取消兄弟的已选
$("#list").on("click", "li", function () {
  $(this)
    .addClass("selected")         // 自己选中
    .siblings()                   // 切到兄弟
    .removeClass("selected")      // 取消兄弟的选中
    .end()                        // 回到自己
    .prependTo("#list");          // 移到列表最前
});

// 实战:批量初始化一组元素
$.each(["#a", "#b", "#c"], function (i, id) {
  $(id)
    .text("第 " + (i + 1) + " 个")
    .css("background", "#eee")
    .on("click", function () { alert(id); });
});

第一个例子是下拉菜单的完整实现:选元素 → 绑事件 → 切到子菜单 → 停动画 → 滑动展开,一气呵成。第二个例子展示了 end() 在"切回原选区"时的不可替代性——不写 end() 就要存两个变量。

5. 什么时候不要用链式调用

链式调用虽优雅,但不是越多越好。下面几种情况应该拆开写:

// 什么时候不要链式调用

// 1. 链太长,可读性下降
// 反例:
$("#x").find(".a").children().first().next().find("span").addClass("y");
// 改写成多步,加注释:
const $x = $("#x");
const $a = $x.find(".a");
const $target = $a.children().first().next().find("span");
$target.addClass("y");

// 2. 取值操作不返回 jQuery,无法继续链
const text = $("#title").text();      // 返回字符串,后面不能 .xxx
const isOn = $("#cb").prop("checked"); // 返回布尔
const width = $("#box").width();       // 返回数字
// 这些"断链"是正常的,代表你在取数据而非操作元素

// 3. 异步操作不要假装能链
$.get("/api").done(fn).fail(fn2);     // 这是 Promise 链,不是 jQuery 对象链

// 4. 复杂逻辑用变量更清晰
// 老老实实拆开,可维护性比炫技重要

核心原则:可读性优先于炫技。链超过 5 步、含取值操作、逻辑复杂时,老老实实拆成多个变量。十年前的 jQuery 圈曾流行"一行代码搞定一切"的炫技风,留下的代码后人维护起来叫苦连天——别重蹈覆辙。

6. 链式调用的影响

jQuery 把链式调用带到了主流,之后无数库纷纷模仿:

反过来说,现代框架(React/Vue)却抛弃了链式调用——它们走"声明式"路线,强调数据驱动而非命令式操作。理解 jQuery 链式 → React 声明式的转变,就理解了过去十年前端范式的演进。

小结

链式调用是 jQuery 的灵魂:每个方法返回 this 让操作流水线化,end()addBack() 让选区切换灵活。会读链式代码、能在适当地方用 end 简化,就能写出地道的 jQuery。下一篇是本系列最后一篇——插件开发,看 jQuery 如何通过 $.fn 扩展出整个生态。

← 上一篇 jQuery AJAX

下一篇 jQuery 插件

✈️💬