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 把链式调用带到了主流,之后无数库纷纷模仿:
- Lodash / Underscore:方法链处理集合数据,思路完全一致。
- Moment.js / Day.js:日期对象链式调用,
dayjs().add(1, 'day').format()。 - D3.js:选区 + 链式操作 DOM,明显借鉴 jQuery。
- Stream / RxJS:函数式编程里的"管道",本质也是链式。
反过来说,现代框架(React/Vue)却抛弃了链式调用——它们走"声明式"路线,强调数据驱动而非命令式操作。理解 jQuery 链式 → React 声明式的转变,就理解了过去十年前端范式的演进。
小结
链式调用是 jQuery 的灵魂:每个方法返回 this 让操作流水线化,end() 和 addBack() 让选区切换灵活。会读链式代码、能在适当地方用 end 简化,就能写出地道的 jQuery。下一篇是本系列最后一篇——插件开发,看 jQuery 如何通过 $.fn 扩展出整个生态。
← 上一篇 jQuery AJAX
下一篇 jQuery 插件 →