jQuery 的链式调用能写成 $('div').addClass('active').css('color','red').show(),并不是因为 JavaScript 对点号有特殊处理,而是因为这一串方法中的每一个都返回了当前实例。只要理解了返回 this 的机制,就可以用原生 JavaScript 复刻出类似能力。本文将先从链式调用的底层逻辑出发,再拆解 jQuery 的内部设计,最后手写一个支持链式操作的 MiniQuery 库。

链式调用的本质是返回当前实例
在 JavaScript 中,对象方法若返回 undefined,下一次调用会报错;若返回普通值,则链式调用会作用在另一个对象上。链式调用的关键就是让方法返回同一个对象,也就是 this。
下面用一个简单的数值容器说明。构造函数 Box 接收初始值,原型上的 add 和 multiply 在执行完计算后都返回 this。因此可以一直点下去,所有操作都累积在同一个实例的 value 上。
function Box(value) {
this.value = value;
}
Box.prototype.add = function (n) {
this.value += n;
return this;
};
Box.prototype.multiply = function (n) {
this.value *= n;
return this;
};
var box = new Box(1);
box.add(2).multiply(3).add(1);
console.log(box.value); // 10
这个例子中 box.add(2) 返回的是 box 自身,紧随其后的 .multiply(3) 仍然是对同一个 box 进行操作。如果把其中一个方法改成返回数字,链路就会在这里断开。因此链式调用可以理解为一种对象状态持续共享的方法设计,而不是语法特性。
jQuery 把这种设计用在 DOM 集合上:一个 jQuery 对象内部用数字下标保存多个 DOM 节点,并用 length 记录数量。像 addClass、css、each 这些方法本质上都是遍历当前集合,执行操作后再返回当前实例。
jQuery对链式调用的具体设计
jQuery 的工厂函数 $ 每次都会创建新实例,但它的构造函数不是直接暴露的,而是通过原型上的 init 方法完成初始化。核心代码可以简化为如下结构。
function jQuery(selector) {
return new jQuery.fn.init(selector);
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
length: 0,
init: function (selector) {
if (!selector) return this;
if (selector.nodeType) {
this[0] = selector;
this.length = 1;
} else {
var nodes = document.querySelectorAll(selector);
for (var i = 0; i < nodes.length; i++) {
this[i] = nodes[i];
}
this.length = nodes.length;
}
return this;
},
each: function (callback) {
for (var i = 0; i < this.length; i++) {
callback.call(this[i], i, this[i]);
}
return this;
},
addClass: function (name) {
return this.each(function () {
this.classList.add(name);
});
}
};
jQuery.fn.init.prototype = jQuery.fn;
jQuery.fn 是 jQuery.prototype 的别名。为了让 new jQuery.fn.init(selector) 生成的实例能够使用所有原型方法,jQuery 把 init 的原型指向了 jQuery.fn。这样每次调用 $('div') 都会得到一个带 DOM 下标的实例,它继承了 addClass、each 等方法。
这里还有一个重要设计:init 最后返回 this,其他遍历方法也返回 this。当写 $('div').addClass('x').hide() 时,addClass 执行完返回的是同一个 jQuery 实例,hide 才能继续被访问。
jQuery 还设计了 pushStack 和 end 方法,用来处理 find、filter 这类会改变集合内容的操作。它们不能直接返回原实例,否则会把新的节点错误地写回原对象;但如果返回一个完全独立的对象,又无法回到上一层集合。因此 jQuery 会在新对象上保存 prevObject,让 end 可以回退到之前的集合。
手写一个支持链式操作的简易库
下面实现一个 MiniQuery,它支持选择器查询、样式操作、事件绑定、查找和回退,所有核心方法都保持链式返回。代码使用了构造函数加原型的模式,避免引入复杂依赖。
(function (global) {
function MiniQuery(selector, context) {
return new MiniQuery.fn.init(selector, context);
}
MiniQuery.fn = MiniQuery.prototype = {
constructor: MiniQuery,
length: 0,
prevObject: null,
init: function (selector, context) {
if (!selector) return this;
if (selector.nodeType) {
this[0] = selector;
this.length = 1;
return this;
}
if (typeof selector === 'string') {
var root = context || document;
var nodes = root.querySelectorAll(selector);
for (var i = 0; i < nodes.length; i++) {
this[i] = nodes[i];
}
this.length = nodes.length;
}
return this;
},
each: function (callback) {
for (var i = 0; i < this.length; i++) {
callback.call(this[i], i, this[i]);
}
return this;
},
addClass: function (className) {
return this.each(function () {
this.classList.add(className);
});
},
removeClass: function (className) {
return this.each(function () {
this.classList.remove(className);
});
},
css: function (name, value) {
if (typeof name === 'string' && value !== undefined) {
return this.each(function () {
this.style[name] = value;
});
}
if (typeof name === 'object') {
var self = this;
Object.keys(name).forEach(function (key) {
self.each(function () {
this.style[key] = name[key];
});
});
return this;
}
if (typeof name === 'string' && value === undefined) {
return this.length > 0 ? this[0].style[name] : undefined;
}
return this;
},
on: function (eventName, handler) {
return this.each(function () {
this.addEventListener(eventName, handler, false);
});
},
find: function (selector) {
var result = MiniQuery();
var list = [];
this.each(function () {
var childNodes = this.querySelectorAll(selector);
for (var i = 0; i < childNodes.length; i++) {
list.push(childNodes[i]);
}
});
result.prevObject = this;
for (var j = 0; j < list.length; j++) {
result[j] = list[j];
}
result.length = list.length;
return result;
},
end: function () {
return this.prevObject || MiniQuery();
},
get: function (index) {
if (index === undefined) {
var arr = [];
for (var i = 0; i < this.length; i++) {
arr.push(this[i]);
}
return arr;
}
return this[index];
}
};
MiniQuery.fn.init.prototype = MiniQuery.fn;
global.MiniQuery = MiniQuery;
})(window);
// 使用示例
MiniQuery('.item').addClass('active').css('color', 'red').on('click', function () {
console.log('clicked');
});
MiniQuery('.list').find('li').addClass('child').end().addClass('parent');
这个库的核心仍然是返回 this。addClass、removeClass、on 都通过 each 遍历集合,并在处理结束后返回当前实例。因此调用方可以一直追加后续操作。
find 方法没有返回原实例,而是新建了一个 MiniQuery 对象。新对象保存了查询到的子元素,同时把 prevObject 指向调用 find 前的集合。这样写 MiniQuery('.list').find('li').addClass('child').end().addClass('parent') 时,end() 会回到 .list 对应的集合上,给父级加上 parent 类名。这种设计让链式操作不仅能前进,还能后退。
css 方法展示了读取和写入两种形态:当第二个参数存在时,它遍历集合并返回 this;当只传一个字符串时,它返回第一个元素的样式值。读取操作返回字符串或 undefined,意味着后续不能继续链式调用,这是 jQuery 中常见的 getter 与 setter 分离策略。
链式断点与设计取舍
并不是所有方法都适合返回 this。像 get() 返回原生 DOM 节点或数组,调用者一旦获取原生对象,就脱离了库的封装,链式调用自然结束。类似地,任何返回布尔值、数值或字符串的方法都会断开链。jQuery 中读取类方法通常返回具体值,写入类方法返回 jQuery 对象,这是刻意的设计。
要让链式调用可回退,需要在改变集合的方法里维护前一个对象。本文使用单个 prevObject,只支持一次回退。jQuery 实际使用 pushStack 管理栈结构,可以连续多次回退。理解了这一点后,可以在 find 中调用一个统一的 pushStack 方法,把新集合和旧集合一起保存,便于扩展。
链式调用虽然让代码紧凑,但过度使用会降低可读性,尤其是在一长串方法中混入异步操作或复杂回调时。调试时,可以在中间断点处用变量暂存当前对象,例如 var items = MiniQuery('.item').addClass('active'); 再继续处理。这样既保留链式风格,也方便排查每一步的集合状态。
另一个常见误区是误以为返回 this 会带来性能问题。实际上原型方法共享同一个函数对象,返回 this 只是返回了一个引用,不会复制 DOM 节点或集合内容。真正需要关注的是每次方法内部遍历是否有重复查询,必要时可以缓存选择器结果,减少 DOM 操作。

