jQuery 源码优秀实践

jQuery 源码中值得学习的设计模式和编程技巧

问题

jQuery 源码中有哪些值得学习的优秀实践?

解答

1. 无 new 构造

调用 jQuery()new jQuery() 效果相同,降低使用门槛。

(function(window) {
  var jQuery = function(selector) {
    // 内部自动 new 一个实例
    return new jQuery.fn.init(selector);
  };

  jQuery.fn = jQuery.prototype = {
    constructor: jQuery,
    init: function(selector) {
      // 选择器逻辑
      var elements = document.querySelectorAll(selector);
      for (var i = 0; i < elements.length; i++) {
        this[i] = elements[i];
      }
      this.length = elements.length;
      return this;
    }
  };

  // 让 init 的实例能访问 jQuery.prototype 上的方法
  jQuery.fn.init.prototype = jQuery.fn;

  window.$ = window.jQuery = jQuery;
})(window);

// 使用时无需 new
$('.box').addClass('active');

2. 链式调用

方法返回 this,支持连续调用。

jQuery.fn.extend({
  addClass: function(className) {
    for (var i = 0; i < this.length; i++) {
      this[i].classList.add(className);
    }
    return this; // 返回 this 实现链式调用
  },
  removeClass: function(className) {
    for (var i = 0; i < this.length; i++) {
      this[i].classList.remove(className);
    }
    return this;
  },
  css: function(prop, value) {
    for (var i = 0; i < this.length; i++) {
      this[i].style[prop] = value;
    }
    return this;
  }
});

// 链式调用
$('.box')
  .addClass('active')
  .removeClass('zzinb')
  .css('color', 'red');

3. 方法重载

同一方法根据参数不同执行不同逻辑。

jQuery.fn.css = function(prop, value) {
  // 无参数:获取第一个元素的所有样式
  if (arguments.length === 0) {
    return getComputedStyle(this[0]);
  }
  
  // 一个参数
  if (arguments.length === 1) {
    // 字符串:获取样式值
    if (typeof prop === 'string') {
      return getComputedStyle(this[0])[prop];
    }
    // 对象:批量设置
    if (typeof prop === 'object') {
      for (var key in prop) {
        this.css(key, prop[key]);
      }
      return this;
    }
  }
  
  // 两个参数:设置单个样式
  for (var i = 0; i < this.length; i++) {
    this[i].style[prop] = value;
  }
  return this;
};

// 多种调用方式
$('.box').css('color');              // 获取
$('.box').css('color', 'red');       // 设置单个
$('.box').css({ color: 'red', fontSize: '14px' }); // 批量设置

4. 延迟对象 Deferred

Promise 的前身,优雅处理异步。

function Deferred() {
  var callbacks = [];
  var state = 'pending';
  var value;

  return {
    resolve: function(val) {
      if (state !== 'pending') return;
      state = 'resolved';
      value = val;
      callbacks.forEach(function(cb) {
        cb.done && cb.done(value);
      });
    },
    reject: function(val) {
      if (state !== 'pending') return;
      state = 'rejected';
      value = val;
      callbacks.forEach(function(cb) {
        cb.fail && cb.fail(value);
      });
    },
    done: function(fn) {
      if (state === 'resolved') {
        fn(value);
      } else {
        callbacks.push({ done: fn });
      }
      return this;
    },
    fail: function(fn) {
      if (state === 'rejected') {
        fn(value);
      } else {
        callbacks.push({ fail: fn });
      }
      return this;
    }
  };
}

// 使用
var dfd = Deferred();
dfd.done(function(data) {
  console.log('成功:', data);
}).fail(function(err) {
  console.log('失败:', err);
});

setTimeout(function() {
  dfd.resolve('数据加载完成');
}, 1000);

5. 插件扩展机制

通过 $.fn.extend$.extend 扩展功能。

// 扩展工具方法
jQuery.extend = function(target) {
  var sources = Array.prototype.slice.call(arguments, 1);
  sources.forEach(function(source) {
    for (var key in source) {
      if (source.hasOwnProperty(key)) {
        target[key] = source[key];
      }
    }
  });
  return target;
};

// 扩展实例方法
jQuery.fn.extend = function(obj) {
  return jQuery.extend(jQuery.fn, obj);
};

// 添加工具方法
$.extend({
  isArray: Array.isArray,
  isFunction: function(fn) {
    return typeof fn === 'function';
  }
});

// 添加插件
$.fn.extend({
  highlight: function(color) {
    return this.css('background', color || 'yellow');
  }
});

// 使用插件
$('.text').highlight('pink');

6. 类型判断封装

统一的类型检测方法。

var class2type = {};
var toString = Object.prototype.toString;

// 生成类型映射
['Boolean', 'Number', 'String', 'Function', 'Array', 'Date', 'RegExp', 'Object', 'Error']
  .forEach(function(name) {
    class2type['[object ' + name + ']'] = name.toLowerCase();
  });

function type(obj) {
  if (obj == null) {
    return obj + ''; // 'null' 或 'undefined'
  }
  return typeof obj === 'object' || typeof obj === 'function'
    ? class2type[toString.call(obj)] || 'object'
    : typeof obj;
}

// 使用
type([]);        // 'array'
type({});        // 'object'
type(null);      // 'null'
type(/abc/);     // 'regexp'

7. 数据缓存

避免在 DOM 上直接存储数据,防止内存泄漏。

var cache = {};
var expando = 'jQuery' + Date.now(); // 唯一标识
var uid = 1;

jQuery.fn.extend({
  data: function(key, value) {
    // 获取
    if (value === undefined) {
      var id = this[0][expando];
      return id && cache[id] ? cache[id][key] : undefined;
    }
    
    // 设置
    for (var i = 0; i < this.length; i++) {
      var elem = this[i];
      // 给元素打上唯一标记
      if (!elem[expando]) {
        elem[expando] = uid++;
      }
      var id = elem[expando];
      if (!cache[id]) {
        cache[id] = {};
      }
      cache[id][key] = value;
    }
    return this;
  },
  
  removeData: function(key) {
    for (var i = 0; i < this.length; i++) {
      var id = this[i][expando];
      if (id && cache[id]) {
        delete cache[id][key];
      }
    }
    return this;
  }
});

// 使用
$('.box').data('info', { name: 'test' });
$('.box').data('info'); // { name: 'test' }

关键点

  • 无 new 构造:内部通过 new jQuery.fn.init() 实现,简化 API 调用
  • 链式调用:方法返回 this,支持流畅的连续操作
  • 方法重载:根据参数类型和数量执行不同逻辑,一个方法多种用法
  • 插件机制:通过原型扩展实现插件系统,易于扩展
  • 数据缓存:用独立对象存储数据,避免 DOM 属性污染和内存泄漏