在15年之前H5及三大框架流行起来之前,jQuery 是前端不可或缺的技能之一, 据不完全统计至今为止世界上至少还有80%的网站由jQuery构成。jQuery对于现在来说也很有借鉴意义,所以就在网上搜集一些资料整理学习一下。

*本文略有增减*

一、整体架构

jQuery 整体框架的结构十分清晰,按代码行文大致分为如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
(function(window, undefined){
//变量、常量、正则初始化
var

//对外提供的接口
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery );
};

//jQuery对象添加方法和属性

//jQuery的继承方法

//jQuery.extend 扩展一些工具的方法(静态方法)

//Sizzle 复杂选择器的实现

//Callbacks 回调对象--函数的统一管理

//Deferred 延迟对象,对异步的统一管理

//support 功能检测

//Data 数据缓存

//queue 队列管理

//Attribute 属性操作

//Event 事件处理

//DOM 操作

//CSS 操作

//ajax

//动画

//坐标和大小

//模块化
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
module.exports = jQuery;
} else {
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function () { return jQuery; } );
}
}

if ( typeof window === "object" && typeof window.document === "object" ) {
window.jQuery = window.$ = jQuery;
}

})(window);


二、jQuery闭包结构

// 用一个函数域包起来,就是所谓的沙箱
// 在这里边 var 定义的变量,属于这个函数域内的局部变量,避免污染全局
// 把当前沙箱需要的外部变量通过函数参数引入进来
// 只要保证参数对内提供的接口的一致性,你还可以随意替换传进来的这个参数

第1种写法
1
2
3
(function(window, undefined) {
// jQuery 代码
})(window);

jQuery 具体的实现,都被包含在了一个立即执行函数构造的闭包里面,为了不污染全局作用域,只在后面暴露 $ 和 jQuery 这 2 个变量给外界,尽量的避开变量冲突。常用的还有另一种写法:

第2种写法
1
2
3
(function(window) {
// JS代码
})(window, undefined);

比较推崇的的第一种写法,也就是 jQuery 的写法。二者有何不同呢,当我们的代码运行在更早期的环境当中(pre-ES5,eg. Internet Explorer 8),undefined 仅是一个变量且它的值是可以被覆盖的。意味着你可以做这样的操作:
undefined = 42;console.log(undefined) // 42

当使用第一种方式,可以确保你需要的 undefined 确实就是 undefined。
这也是 undefined 和 null 的区别之一

另外不得不提出的是,jQuery 在这里有一个针对压缩优化细节,使用第一种方式,在代码压缩的时候,window 和 undefined 都可以压缩为 1 个字母并且确保它们就是 window 和 undefined。

// 压缩策略

1
2
3
4
// w -> windwow , u -> undefined
(function(w, u) {
···
})(window);


三、jQuery无new构造

嘿,回想一下使用 jQuery 的时候,实例化一个 jQuery 对象的方法:

1
2
3
4
5
6
// 无 new 构造
$('#test').text('Test');

// 当然也可以使用 new
var test = new $('#test');
test.text('Test');

大部分人使用 jQuery 的时候都是使用第一种无 new 的构造方式,直接$('')进行构造,这也是 jQuery 十分便捷的一个地方。当我们使用第一种无 new 构造方式的时候,其本质就是相当于 new jQuery(),那么在 jQuery 内部是如何实现的呢?看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function(window, undefined) {
var
// ...
jQuery = function(selector, context) {
// The jQuery object is actually just the init constructor 'enhanced'
// 看这里,实例化方法 jQuery() 实际上是调用了其拓展的原型方法 jQuery.fn.init
return new jQuery.fn.init(selector, context, rootjQuery);
},

// jQuery.prototype 即是 jQuery 的原型,挂载在上面的方法,即可让所有生成的 jQuery 对象使用
jQuery.fn = jQuery.prototype = {
// 实例化化方法,这个方法可以称作 jQuery 对象构造器
init: function(selector, context, rootjQuery) {
// ...
}
}
// 这一句很关键,也很绕
// jQuery 没有使用 new 运算符将 jQuery 实例化,而是直接调用其函数
// 要实现这样,那么 jQuery 就要看成一个类,且返回一个正确的实例
// 且实例还要能正确访问 jQuery 类原型上的属性与方法
// jQuery 的方式是通过原型传递解决问题,把 jQuery 的原型传递给jQuery.prototype.init.prototype
// 所以通过这个方法生成的实例 this 所指向的仍然是 jQuery.fn,所以能正确访问 jQuery 类原型上的属性与方法
jQuery.fn.init.prototype = jQuery.fn;

})(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方法的重载

jQuery 源码晦涩难读的另一个原因是,使用了大量的方法重载,但是用起来却很方便:

1
2
3
4
5
6
7
8
9
// 获取 title 属性的值
$('#id').attr('title');
// 设置 title 属性的值
$('#id').attr('title','jQuery');

// 获取 css 某个属性的值
$('#id').css('title');
// 设置 css 某个属性的值
$('#id').css('width','200px');

方法的重载即是一个方法实现多种功能,经常又是 get 又是 set,虽然阅读起来十分不易,但是从实用性的角度考虑,这也是为什么 jQuery 如此受欢迎的原因,大多数人使用 jQuery() 构造方法使用的最多的就是直接实例化一个 jQuery 对象,但其实在它的内部实现中,有着 9 种不同的方法重载场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接受一个字符串,其中包含了用于匹配元素集合的 CSS 选择器
jQuery([selector,[context]])

// 传入单个 DOM
jQuery(element)

// 传入 DOM 数组
jQuery(elementArray)

// 传入 JS 对象
jQuery(object)

// 传入 jQuery 对象
jQuery(jQuery object)

// 传入原始 HTML 的字符串来创建 DOM 元素
jQuery(html,[ownerDocument])
jQuery(html,[attributes])

// 传入空参数
jQuery()

// 绑定一个在 DOM 文档载入完成后执行的函数
jQuery(callback)


五、jQuery.(fn.)extend

extend方法在jQuery中是一个很重要的方法,jQuey内部用它来扩展静态方法或实例方法,而且我们开发jQuery插件开发的时候也会用到它。但是在内部,是存在jQuery.fn.extendjQuery.extend两个 extend 方法的,而区分这两个 extend 方法是理解 jQuery 的很关键的一部分。先看结论:

1)jQuery.extend(object) 为扩展jQuery类本身,为类添加新的静态方法
2)jQuery.fn.extend(object) 给jQuery对象添加实例方法,也就是通过这个extend添加的新方法,实例化的jQuery对象都能使用,因为它是挂载在 jQuery.fn上的方法.
jQuery.fn = jQuery.prototype

它们的官方解释是:
1)jQuery.extend():把两个或者更多的对象合并到第一个当中,
2)jQuery.fn.extend():把对象挂载到 jQuery 的 prototype 属性,来扩展一个新的 jQuery 实例方法。

使用 jQuery.extend() 拓展的静态方法,我们可以直接使用 $.xxx 进行调用。
使用 jQuery.fn.extend() 拓展的实例方法,需要使用 $().xxx 调用。


六、链式调用及回溯

这一点的实现相对来说比较简单,只需要在要实现链式调用的方法的返回结果里,返回this,就能够实现链式调用了。

除了链式调用,jQuery 甚至还允许回溯:

1
2
// 通过 end() 方法终止在当前链的最新过滤操作,返回上一个对象集合
$('div').eq(0).show().end().eq(1).hide();

当选择了 (‘div’).eq(0) 之后使用 end() 可以回溯到上一步选中的 jQuery 对象 $(‘div’),其内部实现其实是依靠添加了 prevObject 这个属性。
jQuery 完整的链式调用、增栈、回溯通过 return this 、 return this.pushStack() 、return this.prevObject 实现。

总体来说:
1)end() 方法返回 prevObject 属性,这个属性记录了上一步操作的 jQuery 对象合集;
2)而 prevObject 属性由 pushStack() 方法生成,该方法将一个 DOM 元素集合加入到 jQuery 内部管理的一个栈中,通过改变 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合;
3)当我们在链式调用 end() 方法后,内部就返回当前 jQuery 对象的 prevObject 属性,完成回溯。


七、jQuery正则与细节优化

不得不提 jQuery 在细节优化上做的很好,也存在很多值得学习的小技巧。

然后想谈谈正则表达式,jQuery 当中用了大量的正则表达式,我觉得如果研读 jQuery ,正则水平一定能够大大提升,如果是个正则小白,我建议在阅读之前先去了解以下几点:

1)了解并尝试使用 Javascript 正则相关 API,包括了 test() 、replace() 、match() 、exec() 的用法;
2)区分上面 4 个方法,哪个是 RegExp 对象方法,哪个是 String 对象方法;
3)了解简单的零宽断言,了解什么是匹配不捕获以及匹配且捕获


八、jQuery 变量冲突处理

最后想提一提 jQuery 变量的冲突处理,通过一开始保存全局变量的 window.jQuery 以及 windw.$ 。
当需要处理冲突的时候,调用静态方法 noConflict(),让出变量的控制权,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(function(window, undefined) {
var
// Map over jQuery in case of overwrite
// 设置别名,通过两个私有变量映射了 window 环境下的 jQuery 和 $ 两个对象,以防止变量被强行覆盖
_jQuery = window.jQuery,
_$ = window.$;

jQuery.extend({
// noConflict() 方法让出变量 $ 的 jQuery 控制权,这样其他脚本就可以使用它了
// 通过全名替代简写的方式来使用 jQuery
// deep -- 布尔值,指示是否允许彻底将 jQuery 变量还原(移交 $ 引用的同时是否移交 jQuery 对象本身)
noConflict: function(deep) {
// 判断全局 $ 变量是否等于 jQuery 变量
// 如果等于,则重新还原全局变量 $ 为 jQuery 运行之前的变量(存储在内部变量 _$ 中)
if (window.$ === jQuery) {
// 此时 jQuery 别名 $ 失效
window.$ = _$;
}
// 当开启深度冲突处理并且全局变量 jQuery 等于内部 jQuery,则把全局 jQuery 还原成之前的状况
if (deep && window.jQuery === jQuery) {
// 如果 deep 为 true,此时 jQuery 失效
window.jQuery = _jQuery;
}
// 这里返回的是 jQuery 库内部的 jQuery 构造函数(new jQuery.fn.init())
// 像使用 $ 一样尽情使用它吧
return jQuery;
}
})
}(window)

那么让出了这两个符号之后,是否就不能在我们的代码中使用 jQuery 或者呢 $ 呢?莫慌,还是可以使用的:

1
2
3
4
5
6
// 让出 jQuery 、$ 的控制权不代表不能使用 jQuery 和 $ ,方法如下:
var query = jQuery.noConflict(true);
(function($) {
// 插件或其他形式的代码,也可以将参数设为 jQuery
})(query);
// ... 其他用 $ 作为别名的库的代码


文章转载: