学习jQuery源码的收获
最近学习了jQuery源码,其明晰的结构,高内聚、低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷、渐进增强)优雅的处理能力以及 Ajax 等方面周到而强大的定制功能无不令人惊叹。另外,阅读源码让我接触到了大量底层的知识。对原生JS 、框架设计、代码优化有了全新的认识。
本人资历尚浅,无法对 jQuery 分析的头头是道,但是 jQuery 源码当中确实有着大量巧妙的设计,不同层次水平的阅读者都将会有不同的收获,所以本人厚着脸皮将自己从中学到的一些点点滴滴的小知识点共享出来。
【滴水穿石非一日之功,冰冻三尺非一日之寒】
一、jQuery 闭包结构
jQuery 具体的实现,都被包含在了一个立即执行函数构造的闭包里面,为了不污染全局作用域,只在后面暴露 $ 和 jQuery 这 2 个变量给外界,尽量的避开变量冲突。
window.$ = window.jQuery = $; //向外提供接口,将$挂在window下,外部就可以使用$和jQuery
(function(window , undefined){ // 把当前局部代码块需要的外部变量通过函数参数引入进来
...
// jQuery源码
...
})(window);
【1】jQuery源码采取闭包形式(匿名函数自执行),好处是里面的代码都是局部的块,局部的变量、函数都不会与外部同名的变量、函数冲突。
【2】为什么要传入window?
根据函数作用域链,若没有传入window,就会一层一层向上查找window,而window对象是最顶层的对象,查找速度就会非常慢,传入后,只需要查找参数即可。
【3】为什么传入undefined?
在某些浏览器版本中undefined可以被修改
var undefined = 10; alert(undefined); //IE10弹出undefined,IE7 8弹出10
为防止undefined被修改,这里传入undefined,jQuery首先找到的是参数中传入的undefined,不会找到外层或被修改的undefined。
【5】jQuery(document)可以直接使用,但为什么每一个jQuery对象都被赋值给了一个变量?
例如:
var rootJQuery = jQuery(document)
将 jQuery(document) 赋值给 rootJQuery ,实际上是为压缩文件做准备, jQuery(document)是不能被压缩成一个字符的,而rootJQuery可以。
另外,jQuery 在传参时有一个针对压缩的优化细节,在代码压缩的时候,window 和 undefined 都可以压缩为 1 个字母并且确保它们就是 window 和 undefined。这种做法是很有意义的,每使用一次w、u来代替原先较长的名称,就可以节省一点文件大小,累计起来,作用不可小觑。
// w -> windwow , u -> undefined (function(w, u) { ... })(window);
二、jQuery 无 new 构造
我们使用 jQuery实例化一个 jQuery 对象的时候,多半使用第一种无new的情况,那jQuery内部是怎么处理这种逻辑的呢?
// 无 new 构造 $('#test').text('Test'); // 当然也可以使用 new var test = new $('#test'); test.text('Test');
(function(window, undefined) { var // ... jQuery = function(selector, context) { // 实例化方法 jQuery() 实际上是调用了其拓展的原型方法 jQuery.fn.init return new jQuery.fn.init(selector, context, rootjQuery); }, // jQuery.prototype 中挂载的方法属性可应用于所有jQuery实例化对象 jQuery.fn = jQuery.prototype = { // 实例化化方法,这个方法可以称作 jQuery 对象构造器 init: function(selector, context, rootjQuery) { // ... } }
jQuery.fn.init.prototype = jQuery.fn;
// 无new的使用方式,要求$()返回一个jQuery实例化对象,我们才能通过$("xxx")使用jQuery.prototype下的各种方法 // jQuery 的方式是通过原型传递解决问题,把 jQuery 的原型传递给jQuery.prototype.init.prototype // 所以通过这个方法生成的实例 this 所指向的仍然是 jQuery.fn,所以能正确访问 jQuery 类原型上的属性与方法 })(window);
重点捋一下这段代码:
1、首先要明确,使用 $('xxx') 这种实例化方式,其内部调用的是 return new jQuery.fn.init(selector, context, rootjQuery) 这一句话,也就是构造实例是交给了 jQuery.fn.init() 方法去完成。
2、将jQuery.fn.init 的 prototype 设置为 jQuery.fn,那么使用 new jQuery.fn.init() 生成的对象的原型对象就是 jQuery.fn ,所以挂载到 jQuery.fn 上面的函数就相当于挂载到 jQuery.fn.init() 生成的 jQuery 对象上,所有使用 new jQuery.fn.init() 生成的对象也能够访问到 jQuery.fn 上的所有原型方法。
3、也就是实例化方法存在这么一个关系链
- jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype ;
- new jQuery.fn.init() 相当于 new jQuery() ;
- jQuery() 返回的是 new jQuery.fn.init(),而 var obj = new jQuery(),所以这 2 者是相当的,所以我们可以无 new 实例化 jQuery 对象。
三、jQuery.fn.extend 与 jQuery.extend
如果现有的jQuery无法满足我们多变的需求,我们就可以通过这两个方法来扩展jQuery,即为jQuery写插件,我们先了解一下两个方法:
jQuery.extend(object) 为扩展 jQuery 类本身,为类添加新的 静态方法;直接使用 $.xxx 进行调用( 如$.trim() )
jQuery.fn.extend(object) 给 jQuery 对象添加实例方法,也就是通过这个 extend 添加的新方法,实例化的 jQuery 对象都能使用,因为它是挂载在 jQuery.fn 上的方法(上文有提到,jQuery.fn = jQuery.prototype )。 使用$().xxx调用( 如$('.header').css() )
// 扩展合并函数 // 合并两个或更多对象的属性到第一个对象中,jQuery 后续的大部分功能都通过该函数扩展 // 虽然实现方式一样,但是要注意区分用法的不一样,那么为什么两个方法指向同一个函数实现,但是却实现不同的功能呢, // 阅读源码就能发现这归功于 this 的强大力量 // 如果传入两个或多个对象,所有对象的属性会被添加到第一个对象 target // 如果只传入一个对象,则将对象的属性添加到 jQuery 对象中,也就是添加静态方法 // 用这种方式,我们可以为 jQuery 命名空间增加新的方法,可以用于编写 jQuery 插件 // 如果不想改变传入的对象,可以传入一个空对象:$.extend({}, object1, object2); // 默认合并操作是不迭代的,即便 target 的某个属性是对象或属性,也会被完全覆盖而不是合并 // 如果第一个参数是 true,则是深拷贝 // 从 object 原型继承的属性会被拷贝,值为 undefined 的属性不会被拷贝 // 因为性能原因,JavaScript 自带类型的属性不会合并 jQuery.extend = jQuery.fn.extend = function() { var src, copyIsArray, copy, name, options, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation // target 是传入的第一个参数 // 如果第一个参数是布尔类型,则表示是否要深递归, if (typeof target === "boolean") { deep = target; target = arguments[1] || {}; // skip the boolean and the target // 如果传了类型为 boolean 的第一个参数,i 则从 2 开始 i = 2; } // Handle case when target is a string or something (possible in deep copy) // 如果传入的第一个参数是 字符串或者其他 if (typeof target !== "object" && !jQuery.isFunction(target)) { target = {}; } // extend jQuery itself if only one argument is passed // 如果参数的长度为 1 ,表示是 jQuery 静态方法 if (length === i) { target = this; --i; } // 可以传入多个复制源 // i 是从 1或2 开始的 for (; i < length; i++) { // Only deal with non-null/undefined values // 将每个源的属性全部复制到 target 上 if ((options = arguments[i]) != null) { // Extend the base object for (name in options) { // src 是源(即本身)的值 // copy 是即将要复制过去的值 src = target[name]; copy = options[name]; // Prevent never-ending loop // 防止有环,例如 extend(true, target, {'target':target}); if (target === copy) { continue; } // Recurse if we're merging plain objects or arrays // 这里是递归调用,最终都会到下面的 else if 分支 // jQuery.isPlainObject 用于测试是否为纯粹的对象 // 纯粹的对象指的是 通过 "{}" 或者 "new Object" 创建的 // 如果是深复制 if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { // 数组 if (copyIsArray) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; // 对象 } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // Never move original objects, clone them // 递归 target[name] = jQuery.extend(deep, clone, copy); // Don't bring in undefined values // 最终都会到这条分支 // 简单的值覆盖 } else if (copy !== undefined) { target[name] = copy; } } } } // Return the modified object // 返回新的 target // 如果 i < length ,是直接返回没经过处理的 target,也就是 arguments[0] // 也就是如果不传需要覆盖的源,调用 $.extend 其实是增加 jQuery 的静态方法 return target; };
需要注意的是 jQuery.extend = jQuery.fn.extend = function() {} ,也就是 jQuery.extend 的实现和 jQuery.fn.extend 的实现共用了同一个方法,但是为什么能够实现不同的功能了,这就要归功于 Javascript 强大(怪异)的 this 了。
1)在 jQuery.extend() 中,this 的指向是 jQuery 对象(或者说是 jQuery 类),所以这里扩展在 jQuery 上;
2)在 jQuery.fn.extend() 中,this 的指向是 fn 对象,前面有提到 jQuery.fn = jQuery.prototype ,也就是这里增加的是原型方法,也就是对象方法。