这篇文章将主要介绍jQuery框架的插件机制,包括但不限于jQuery.extendjQuery.fn.extend方法的设计和使用,JavaScript体系中的常用概念以及jQuery插件的使用等。

1.0 源码解读

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
jQuery.extend = jQuery.fn.extend = function() {
//声明一堆的变量length 为实参的个数
var options, name, src, copy, copyIsArray, clone,
target = arguments[ 0 ] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !isFunction( target ) ) {
target = {};
}
// Extend jQuery itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
if ( ( options = arguments[ i ] ) != null ) {
// Extend the base object
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// Prevent never-ending loop
if ( target === copy ) {
continue;
}
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = Array.isArray( copy ) ) ) ) {
if ( copyIsArray ) {
copyIsArray = false;
clone = src && Array.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
return target;
};
//为jQuery添加一堆工具方法,对象参数中所有的成员都将直接添加在jQuery函数身上成为jQuery的静态方法
jQuery.extend( {
// Unique for each copy of jQuery on the page
// 简单测试下得到的结果是:jQuery33104605303773584173
// 确保页面中的jQuery副本是唯一的,(jQuery + 版本号 + 随机数) => 格式处理
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
// Assume jQuery is ready without the ready module
// 是否已经准备就绪
isReady: true,
//错误处理方法:根据传入的消息创建一个错误对象并抛出异常
error: function( msg ) {
throw new Error( msg );
},
//空函数
noop: function() {},
//用于判断指定参数是否是一个纯粹的对象
//所谓"纯粹的对象",就是该对象是通过"{}"或"new Object"创建的 .排除了数组、自定义构造函数创建的对象以及函数等类型
isPlainObject: function( obj ) {
var proto, Ctor;
// Detect obvious negatives
// Use toString instead of jQuery.type to catch host objects
//如果参数为null | undefined 或者在调用Object.prototype.toString.call(参数)的时候得到的结果不是[object Object]则直接返回false
if ( !obj || toString.call( obj ) !== "[object Object]" ) {
return false;
}
//获取当前对象的原型对象,其实是调用了Object.getPrototypeOf(参数) 方法
proto = getProto( obj );
// Objects with no prototype (e.g., `Object.create( null )`) are plain
//监测 没有原型对象的最纯净的对象 例如使用Object.create( null )创建的对象
if ( !proto ) {
return true;
}
// Objects with prototype are plain iff they were constructed by a global Object function
//如果原型对象上拥有constructor属性(前一句的结果为true) 那么就返回proto.constructor的值
//如果原型对象是Object.prototype 那么Object.prototype.constructor ==> ƒ Object() { [native code] }
Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
//检查Ctor是否是函数 且函数字符串是否全等于function Object() { [native code] }
return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
},
//用于判断指定参数是否是一个空对象
//所谓"空对象",即不包括任何可枚举(自定义)的属性。简而言之,就是该对象没有属性可以通过for...in迭代。
isEmptyObject: function( obj ) {
/* eslint-disable no-unused-vars */
// See https://github.com/eslint/eslint/issues/6125
var name;
for ( name in obj ) {
return false;
}
return true;
},
// Evaluates a script in a global context
//用于全局性地执行一段JavaScript代码,内部调用DOMEval方法实现
//其作用与常规的JavaScript eval()函数相似。区别自傲与jQuery.globalEval()执行代码的作用域为全局作用域。
//使用示例:$.globalEval( "var a =1" ); 该行代码将在全局作用域中定义变量a
globalEval: function( code ) {
DOMEval( code );
},
//常用的迭代方法,可以用来遍历数组|对象|jQ实例对象(伪数组)
//该方法同$("xxx").each() 方法保持一致
//第一个参数:要遍历的对象| 数组 | jQ实例对象
each: function( obj, callback ) {
var length, i = 0;
//如果参数是伪数组那么使用普通的for循环来进行遍历
if ( isArrayLike( obj ) ) {
length = obj.length;
for ( ; i < length; i++ ) {
//调用回到函数,把当前的value值绑定给函数的this [each方法的回调函数中this--> value值]
//把当前循环的key和value值(这里是i和obj[i])作为参数传递给callback回调函数
//检查回调函数的返回值,如果返回的是false,那么就退出循环
if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
break;
}
}
} else {
//如果是普通的对象那么使用for..in循环来进行遍历
for ( i in obj ) {
//同上面的代码保持一致
if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
break;
}
}
}
//把遍历的对象返回,链式编程的代码风格
return obj;
},
// Support: Android <=4.0 only
// 工具方法,用于清空字符串前面或者是后面的N个空格
// 在ES5中js提供了原生的trim方法来清除字符串前后的1个或多个空格
// 这里主要是通过正则表达式去进行匹配,把匹配到的内容替换为空字符串""
trim: function( text ) {
return text == null ?
"" :
( text + "" ).replace( rtrim, "" );
},
// results is for internal usage only
// 结果仅共内部使用
// 该函数用于将一个类数组对象(伪数组)转换为真正的数组对象
// 所谓"类数组对象"就是一个常规的Object对象,但它和数组对象非常相似:具备length属性,并以0、1、2、3……等数字作为属性名。
makeArray: function( arr, results ) {
// 初始化ret为空数组
var ret = results || [];
if ( arr != null ) {
//检查传入的参数是否是伪数组
if ( isArrayLike( Object( arr ) ) ) {
//如果是伪数组,那么合并ret和arr 并返回
//如果arr是字符串那么jQuery.merge(ret,[arr]) ,否则jQuery.merge(ret,arr)
jQuery.merge( ret,
typeof arr === "string" ?
[ arr ] : arr
);
} else {
//如果参数不是伪数组,那么直接把arr中的每个数据都添加到新的数组中
//push.call( ret, arr ) 就是是 [].push.call(ret,arr) => [].push(arr)
push.call( ret, arr );
}
}
//返回处理完的数组对象
return ret;
},
// 该方法用于在数组中搜索指定的值,并返回其索引值。如果数组中不存在该值,则返回 -1。
// 第一个参数 用于查找的值
// 第二个参数 指定被查找的数组
// 第三个参数 指定从数组的指定索引位置开始查找,默认为 0
// 如果数组中存在多个相同的值,则以查找到的第一个值的索引为准
// 使用示例:$.inArray("文顶顶",["demoA","demoB","wendingding.com","文顶顶","demoC","end"],4)
// 上面的代码表示从数组中索引为4的位置开始查找"文顶顶"这个元素项,返回的结果为-1,最后一个参数不传递则返回3
inArray: function( elem, arr, i ) {
//如果参数是null或undefined那么直接返回-1
//否则通过调用indexOf方法实现 indexOf.call( arr, elem, i ) => arr.indexOf(elem,i)
return arr == null ? -1 : indexOf.call( arr, elem, i );
},
// Support: Android <=4.0 only, PhantomJS 1 only
// push.apply(_, arraylike) throws on ancient WebKit
//该方法用于合并两个数组
merge: function( first, second ) {
var len = +second.length,
j = 0,
i = first.length;
for ( ; j < len; j++ ) {
//通过普通的for循环来遍历第二个数组
//把第二个数组的元素依次追加在第一个数组的后面
first[ i++ ] = second[ j ];
}
//更新数组的长度值
first.length = i;
return first;
},
// 过滤函数用于过滤数组
// 参数1 待过滤的数组
// 参数2 过滤数组的具体函数
// 参数3 布尔类型的值
// 为true则函数返回数组中由过滤函数返回 true 的元素
// 为false则函数返回数组中由过滤函数返回false的元素
grep: function( elems, callback, invert ) {
//初始化一堆的变量
var callbackInverse,
matches = [], //空数组
i = 0, //索引值为0
length = elems.length, //待过滤数组的长度
callbackExpect = !invert;
// Go through the array, only saving the items
// that pass the validator function
for ( ; i < length; i++ ) {
//每循环一次就把当前的元素和对应的索引传递给回调函数,并保存回调函数的返回值取反
callbackInverse = !callback( elems[ i ], i );
if ( callbackInverse !== callbackExpect ) {
//把过滤后的元素收集保存到新的数组中
matches.push( elems[ i ] );
}
}
//返回过滤后得到的数据,是一个新的数组
return matches;
},
// arg is for internal usage only
// arg仅用于内部使用的情况
// 数组映射方法( 将一个数组中的元素转换到另一个数组中 )
// 参数1 :待处理的数组
// 参数2 : 具体的处理函数
// 参数3 : arg
map: function( elems, callback, arg ) {
//初始化一堆的变量
var length, value,
i = 0, //索引值为0
ret = []; //ret为空的数组
// Go through the array, translating each of the items to their new values
// 遍历数组把数组中的每一项都转换为一个新的值
//检查是否是伪数组
if ( isArrayLike( elems ) ) {
length = elems.length; //获取待处理的伪数组的长度
for ( ; i < length; i++ ) {
//循环,每循环一次就调用处理函数并把当前的key和value值作为参数传递进去
//收集回调函数的返回值
value = callback( elems[ i ], i, arg );
//如果回调函数的范返回值不为空,那么就把该返回值添加到数组中并最终返回
if ( value != null ) {
ret.push( value );
}
}
// Go through every key on the object,
// 如果是普通的对象,那么就使用for...in循环来进行遍历
} else {
for ( i in elems ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret.push( value );
}
}
}
// Flatten any nested arrays
// 等价于 [].concat(ret) 问题:为什么不直接范湖ret数组呢?
return concat.apply( [], ret );
},
// A global GUID counter for objects
// 全局的GUID计数器
guid: 1,
// jQuery.support is not used in Core but other projects attach their
// properties to it so it needs to exist.
// 就是个空对象 {} 不在核心中使用,但其他项目将它们的属性附加到它,因此它需要存在。
support: support
} );

2.0 jQuery框架的插件处理机制

jQuery框架中上面列出的这几百行代码主要做了两件事情。

❏ 在jQuery的基础上拓展了jQuery.extend方法和jQuery.fn.extend方法。
❏ 调用jQuery.extend方法来为jQuery批量的添加一堆的静态方法(如mapgrep等)。

jQuery插件方法的实现

jQuery官方框架中在实现jQuery.extendjQuery.fn.extend这两个方法的时候用了接近80行代码,具体的实现细节比较复杂,这里给出一个简易版本来帮助大家理解这两个方法都做了什么事情。

1
2
3
4
5
6
7
8
9
10
11
12
jQuery.extend = jQuery.fn.extend = function (objT) {
//遍历参数对象,获取对象中的每个成员添加在jQuery或者是jQuery.fn对象。
for (var key in objT)
{
// 过滤掉原型对象的成员,如果仅仅只处理函数还可以对参数的类型进行判断
if (Object.hasOwnProperty(key))
{
//this的值由函数的调用方式决定
this[key] = objT[key];
}
}
};

其实简单点说,这两个的方法的功能就是把传递给他们的参数(对象类型)中的所有成员都添加到对应的对象中(如果是jQuery.fn.extend方法,那么就添加到jQuery的原型对象身上作为原型成员来使用,如果是jQuery.extend方法,则直接添加到jQuery身上作为静态方法来使用)。

在这两个方法的实现中巧妙利用了this指向的对象由函数调用方式决定这一特点,做到jQuery.extend = jQuery.fn.extend。也因此,虽然在jQuery.extend方法jQuery.fn.extend方法的函数体相同,但由于内部使用了this的缘故,所以在单独调用这两个方法的时候它们是区分处理的。

jQuery插件方法的调用

这里给出上面代码中jQuery.extend()调用的简化版本。

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
jQuery.extend({
//判断指定参数是否是一个纯粹的对象
isPlainObject:function(obj){},
//判断指定参数是否是一个空的对象
isEmptyObject:function(obj){},
//迭代方法,用来遍历对象、数组和伪数组
each:function( obj, callback){},
//清除字符串前后的N个空格,同ES5中字符串的trim方法
trim:function(text){},
//把伪数组对象转换为数组
makeArray:function(arr, results ){},
//该方法用于在数组中搜索指定的值,并返回其索引值。
inArray:function(elem, arr, i) {},
//该方法用于合并两个数组
merge:function(first, second ){},
//这是一个过滤函数,主要用于过滤数组
grep: function( elems, callback, invert ) {}
//数组映射方法,把一个数组中的元素转换到另一个数组中
map:function( elems, callback, arg ) {}
})

jQuery.extend() 和jQuery.fn.extend()这两个方法在jQuery框架中被大量使用,绝大多数的方法均通过调用者两个函数来添加到jQuery身上或者是jQuery的原型对象身上,这种为jQuery和jQuery原型对象拓展方法(功能)的机制被称为插件机制。jQuery框架的官方团队维护了一个jQuery官方插件列表,我们可以在使用jQuery的基础上通过引入对应的jQuery插件来快速实现特定的功能。当然,除jQuery官方团队维护的插件之外,我们还能在互联网上找到很多其它开发人员为jQuery写的一些优秀插件,在具体的开发中可以根据项目的需求来综合评估是否直接使用jQuery插件来实现。

总体来说,jQuery的插件实现是比较简单的。如果你想要自己写一个jQuery插件,那么只需要创建一个新的js文件,该文件推荐命名为jQuery.xx.js的形式。然后在js代码中通过调用jQuery.extend() 和jQuery.fn.extend()方法来拓展功能即可。

常见的插件实现形式是:把对应的代码包裹到闭包中,然后传递jQuery对象。