table of contents

1概述

早几年学习前端,大家都非常热衷于研究 jQuery 源码。我还记得当初从 jQuery 源码中学到一星半点应用技巧的时候常会有一种发自内心的惊叹,“原来 JavaScript 居然可以这样用!”

虽然随着前端的发展,另外几种前端框架的崛起,jQuery 慢慢变得不再是必须,因此大家对于 jQuery 的热情低了很多。但是许多从 jQuery 中学到的技巧用在实践中仍然非常好用。了解它有助于我们更加深入的理解 JavaScript。

这里我们把 jQuery 的实现作为一个学习案例,帮助我们进一步掌握面向对象的使用。也为有兴趣的朋友,进一步学习 jQuery源码一个铺垫,算是一个简单的抛砖引玉。

2基本实现

使用 jQuery 时,我们通常会这样写:

code.ts
1
// 声明一个 jquery 实例
2
$('.target')
3
4
// 获取元素的css属性
5
$('.target').css('width')
6
7
// 获取元素的微信信息
8
$('target').offset()

咦?很奇怪,和普通的对象实例不太一样,new 关键字哪里去了?$ 符号又是什么?

带着这些疑问,我们来实现一个简化版的 jQuery 库。

首先,一个库就是一个单独的模块,使用自执行函数的方式模拟一个模块。

code.ts
1
(function() {
2
// do something
3
});

第二步,我们能够在全局直接调用 jQuery,说明 jQuery 被挂载在了全局对象上。因此当我们在模块中对外提供接口时,可以采取 window.jQuery 的方式。

code.ts
1
// 先声明一个构造函数
2
var jQuery = function() {}
3
4
// ...
5
6
window.jQuery = jQuery

在使用时,并没有用 jQuery,而是使用了 $, 其实是多加了一个赋值操作。

code.ts
window.$ = window.jQuery = jQuery

我们在使用时,直接使用 $,也就是说直接调用了构造函数 jQuery 来创建了一个实例,而没有使用 new。但是我们知道,创建一个实例 new 关键字肯定是必不可少的。因此说明 new 的操作被放在了 jQuery 方法中来实现。而 jQuery 也并不是真正的构造函数。

前面一节的学习我们知道,每一个函数都可能是任何角色,因此 jQuery 内部的实现正是利用了这一点,在具体实现时,改变了内部某些函数的 prototype 指向。先看实现代码,再来一步步分析。

code.ts
1
;(function (ROOT) {
2
// 构造函数
3
var jQuery = function (selector) {
4
// 在该方法中直接返回new过的实例,因此这里的init才是真正的构造函数
5
return new jQuery.fn.init(selector);
6
}
7
8
jQuery.fn = jQuery.prototype = {
9
constructor: jQuery,
10
version: '1.0.0',
11
init: function (selector) {
12
var elem, selector;
13
elem = document.querySelector(selector);
14
this[0] = elem;
15
16
// 在jquery中返回的是一个由所有原型属性方法组成的数组,我们这里做了简化,直接返回this即可
17
return this;
18
},
19
20
// 在原型上添加一堆方法
21
toArray: function () { },
22
get: function () { },
23
each: function () { },
24
ready: function () { },
25
first: function () { },
26
slice: function () { }
27
// ... more
28
}
29
30
// 让init方法的原型,指向jQuery的原型
31
jQuery.fn.init.prototype = jQuery.fn;
32
33
ROOT.jQuery = ROOT.$ = jQuery;
34
})(window);

首先在 jQuery 构造函数中声明了一个 fn 属性,并将其指向了原型 jQuery.prototype。随后在原型对象中添加了init方法。

code.ts
1
jQuery.fn = jQuery.prototype = {
2
init: function() {}
3
}

之后又将 init 的原型,指向了 jQuery.prototype.

code.ts
jQuery.fn.init.prototype = jQuery.fn;

而在构造函数jQuery中,则返回了init的实例对象。

code.ts
1
var jQuery = function(selector) {
2
return new jQuery.fn.init(selector);
3
}

最后对外暴露接口时,将字符 $ 与方法 jQuery 对等起来。

code.ts
ROOT.jQuery = ROOT.$ = jQuery;

因此当我们使用 $('#test') 创建一个 jQuery 实例时,实际上是调用的 jQuery('#test') 创建的一个 init 实例。这里真正的构造函数是原型中的 init 方法。

下面用图例展示下这中间的逻辑变化。

3扩展方法

我们在使用 jQuery 时还知道,jQuery 提供了两个扩展接口来帮助我们自定义 jQuery 的方法。通常称自定义的 jQuery 方法为 jQuery 插件。那么这两个扩展方法是如何实现的呢?在上面的实现基础上我们继续添加代码,如下。

code.ts
1
;
2
(function (ROOT) {
3
4
// 构造函数
5
var jQuery = function (selector) {
6
// 在该方法中直接返回new过的实例,因此这里的init才是真正的构造函数
7
return new jQuery.fn.init(selector);
8
}
9
10
jQuery.fn = jQuery.prototype = {
11
constructor: jQuery,
12
version: '1.0.0',
13
init: function (selector) {
14
var elem, selector;
15
elem = document.querySelector(selector);
16
this[0] = elem;
17
18
// 在jquery中返回的是一个由所有原型属性方法组成的数组,我们这里做了简化,直接返回this即可
19
return this;
20
},
21
22
// 在原型上添加一堆方法
23
toArray: function () { },
24
get: function () { },
25
each: function () { },
26
ready: function () { },
27
first: function () { },
28
slice: function () { }
29
// ... more
30
}
31
32
// 让init方法的原型,指向jQuery的原型
33
jQuery.fn.init.prototype = jQuery.fn;
34
35
// 实现jQuery的两种扩展方法
36
jQuery.extend = jQuery.fn.extend = function (options) {
37
// 在jquery源码中会根据参数不同进行不同的判断,我们这里假设只有一种方式
38
var target = this;
39
var copy;
40
41
for (name in options) {
42
copy = options[name];
43
target[name] = copy;
44
}
45
return target;
46
}
47
48
// jQuery中利用上面实现的扩展机制,添加了许多方法,其中
49
50
// 添加静态扩展方法,即工具方法
51
jQuery.extend({
52
isFunction: function () { },
53
type: function () { },
54
parseHTML: function () { },
55
parseJSON: function () { },
56
ajax: function () { }
57
// ...
58
})
59
60
// 添加原型方法
61
jQuery.fn.extend({
62
queue: function () { },
63
promise: function () { },
64
attr: function () { },
65
prop: function () { },
66
addClass: function () { },
67
removeClass: function () { },
68
val: function () { },
69
css: function () { }
70
// ...
71
})
72
73
ROOT.jQuery = ROOT.$ = jQuery;
74
})(window);

在上面的代码中,我们通过下面的方式简单的实现了两个扩展方法。

code.ts
1
jQuery.extend = jQuery.fn.extend = function (options) {
2
3
// 在jquery源码中会根据参数不同进行很多判断,我们这里就直接走一种方式,所以就不用判断了
4
var target = this;
5
var copy;
6
7
for (name in options) {
8
copy = options[name];
9
target[name] = copy;
10
}
11
return target;
12
}

要理解它的实现,首先要明确的知道内部 this 的指向。相信学习过前面的章节对于 this 的掌握已经不是问题了。传入的参数 options 对象是一个 key-value 模式的对象。我们通过 for in 遍历 options,将 key 作为新的属性,value 作为该属性对应的新方法,分别添加到 jQuery 与 jQuery.fn 中。

也就是说,当我们通过 $.extend 扩展 jQuery 时,方法被添加到了静态方法中,而当我们通过 $.fn.extend 扩展 jQuery 时,方法被添加到了原型对象中。

静态方法可以直接调用,因此常常也被称为工具方法。例如:

code.ts
1
$.ajax()
2
$.isFunction()
3
$.each()
4
//...

原型方法则必须通过声明的实例才能调用。

code.ts
1
$('#test').css();
2
$('#test').attr();
3
4
// ...
专栏首页
到顶
专栏目录