广告位 · 页头(后台-模块管理-新建模块后替换此区域)

jQuery链式调用原理是什么?如何手写一个支持链式操作的简易库

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

jQuery链式调用原理是什么?如何手写一个支持链式操作的简易库

链式调用的本质是返回当前实例

在 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 操作。

广告位 · 文章底部(后台-模块管理替换此区域)