jQuery源码学习(版本1.11)-扩展工具函数

本文聚焦于jQuery 1.11.1版本,详细解读了extend()函数的实现,探讨了该函数如何用于jQuery的继承机制。同时,文章还介绍了extend()扩展的一系列实用工具函数,通过源码分析,帮助读者理解jQuery的核心功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

本文详细分析jquery-1.11.1.js源码文件行数:174~584;

代码简介:定义了extend()函数,并用其扩展了一些常用的工具函数;

下文进行详细代码分析。


extend():jQuery的继承方法

// 同时被jQuery以及jQuery.fn即原型引用,便于后面扩展各自的函数
// 扩展进jQuery的函数,可通过jQuery这个命名空间去调用,称之为工具函数,是jQuery较底层的实现,内部很多其他函数都调用了工具函数
// 扩展进jQuery.fn的函数,是所有JQ对象的公用函数
jQuery.extend = jQuery.fn.extend = function() {
	var src, copyIsArray, copy, name, options, clone,
		// 初始化要扩展的target是第一个参数,如果arguments[0]是false,即表示浅拷贝,则target直接设置成新对象
		target = arguments[0] || {},
		i = 1,
		length = arguments.length,
		// 默认是浅拷贝
		deep = false;

	// 从前面初始化可以看出,如果成立则肯定为深度拷贝,则传给deep
	if ( typeof target === "boolean" ) {
		deep = target;

		// 再将target指向第二个参数
		target = arguments[ i ] || {};
		i++;
	}

	// 兼容target不是对象或函数的场景
	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
		target = {};
	}

	// 由前面的代码可以看出,i永远领先1的,如果i === length成立,则表示设置了target = arguments[i--]||{}之后,就已经没有其他参数可以用于扩展了
	// 则target = this;把需要扩展的对象设置成调用者本身,而顺理成章arguments[i--]成为被扩展的对象
	if ( i === length ) {
		target = this;
		i--;
	}
	
	// 以上的代码都是对arguments的不同情况进行兼容,确定需要扩展的对象target,以及deep,还有i的值
	// i的值用于后面遍历arguments,arguments[i]以上的参数就是被扩展的对象了
	

	for ( ; i < length; i++ ) {
		// options指向arguments[ i ]并进行空判断
		if ( (options = arguments[ i ]) != null ) {
			// 循环遍历options每一个可扩展属性
			for ( name in options ) {
				src = target[ name ];
				copy = options[ name ];

				// 防止循环引用自身
				if ( target === copy ) {
					continue;
				}

				// 处理深度拷贝场景,需要判断copy是否能拷贝,jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) 满足即可
				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
					if ( copyIsArray ) {
						// copyIsArray = false;这行我个人觉得是多余的,后面没有使用copyIsArray的地方了
						copyIsArray = false;
						// 如果src不为空且是类数组结构,则直接将src作为扩展对象进入下一轮递归,原内容会被替换掉
						clone = src && jQuery.isArray(src) ? src : [];

					} else {
						// 同上如果src不为空且是纯粹的对象({}或new Object()建立的),则直接将src作为扩展对象进入下一轮递归,原内容会被替换掉
						clone = src && jQuery.isPlainObject(src) ? src : {};
					}

					// 进入递归
					target[ name ] = jQuery.extend( deep, clone, copy );

				// 被扩展属性不为空则扩展进target[ name ]
				} else if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}
	}

	// 返回扩展之后的target
	return target;
};


扩展一些常用工具函数

jQuery.extend({
	// Unique for each copy of jQuery on the page
	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() {},

	isFunction: function( obj ) {
		return jQuery.type(obj) === "function";
	},

	isArray: Array.isArray || function( obj ) {
		return jQuery.type(obj) === "array";
	},

	isWindow: function( obj ) {
		/* 使用&&先判断obj是否为空,防止出错
		return obj != null && obj == obj.window;
	},

	isNumeric: function( obj ) {
		// 判断obj是否为数字,parseFloat(obj)将null|true|false|""全部转成NaN,这样就必定返回false
		// 如果直接判断obj >= 0,假设obj=true  true > 0是返回true的
		return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0;
	},

	// 使用for in 判断obj是否为empty
	isEmptyObject: function( obj ) {
		var name;
		for ( name in obj ) {
			return false;
		}
		return true;
	},

	// 判断是否为纯粹对象
	isPlainObject: function( obj ) {
		var key;

		// 参数校验,必须是object且不能为dom对象,window对象
		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
			return false;
		}

		try {
			// 一般情况下,constructor属性都是在prototype对象里面的(自身属性一般不定义constructor)
			// 因此hasOwn.call(obj, "constructor")都一般为false,即可用obj.constructor.prototype判断isPrototypeOf,否则不适用判断isPrototypeOf
			// isPrototypeOf属性一般存在Object的原型里,通过obj.constructor.prototype找到obj原型,然后hasOwn判断则确定是否为纯粹对象
			// 原型重新定义过,或者通过继承得到的新对象的实例,hasOwn.call(obj.constructor.prototype, "isPrototypeOf")都会返回false
			if ( obj.constructor &&
				!hasOwn.call(obj, "constructor") &&
				!hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
				return false;
			}
		} catch ( e ) {
			return false;
		}

		// IE < 9下,自身属性枚举会靠后,判断第一个即可
		if ( support.ownLast ) {
			for ( key in obj ) {
				return hasOwn.call( obj, key );
			}
		}

		// Own properties枚举时都会优先,因此判断最后一个可枚举的属性即可
		for ( key in obj ) {}

		return key === undefined || hasOwn.call( obj, key );
	},

	type: function( obj ) {
		// undefined或null则将其转换成字符串返回
		if ( obj == null ) {
			return obj + "";
		}
		
		// 从class2type取出对应值返回,相比直接返回typeof值,利用class2type可以识别更多的BOM对象
		return typeof obj === "object" || typeof obj === "function" ?
			class2type[ toString.call(obj) ] || "object" :
			typeof obj;
	},

	// Evaluates a script in a global context
	// Workarounds based on findings by Jim Driscoll
	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
	globalEval: function( data ) {
		if ( data && jQuery.trim( data ) ) {
			// We use execScript on Internet Explorer
			// We use an anonymous function so that context is window
			// rather than jQuery in Firefox
			( window.execScript || function( data ) {
				window[ "eval" ].call( window, data );
			} )( data );
		}
	},

	// 改为驼峰写法,用于后面css和data modules
	// rmsPrefix兼容IE
	camelCase: function( string ) {
		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
	},

	// 判断elem.nodeName.toLowerCase() === name.toLowerCase()
	nodeName: function( elem, name ) {
		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
	},

	// each会对传入的obj执行callback回调函数,args则是callback的参数
	each: function( obj, callback, args ) {
		var value,
			i = 0,
			length = obj.length,
			isArray = isArraylike( obj );

		// 分场景处理,args存在
		if ( args ) {
			// isArray为true则用遍历数组方法遍历obj,apply执行callback
			if ( isArray ) {
				for ( ; i < length; i++ ) {
					value = callback.apply( obj[ i ], args );

					// 可用于及早停止循环
					if ( value === false ) {
						break;
					}
				}
			} else {
				// isArray为false则用for in 遍历obj,执行callback
				for ( i in obj ) {
					value = callback.apply( obj[ i ], args );

					if ( value === false ) {
						break;
					}
				}
			}

		// args不存在,则把i,以及自身obj[ i ]作参数,调用call传给callback执行
		} else {
			if ( isArray ) {
				for ( ; i < length; i++ ) {
					value = callback.call( obj[ i ], i, obj[ i ] );

					if ( value === false ) {
						break;
					}
				}
			} else {
				for ( i in obj ) {
					value = callback.call( obj[ i ], i, obj[ i ] );

					if ( value === false ) {
						break;
					}
				}
			}
		}

		return obj;
	},

	// rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,其中\uFEFF是字节序标记,\xA0是拉丁文中的空白
	trim: function( text ) {
		return text == null ?
			"" :
			( text + "" ).replace( rtrim, "" );
	},

	// makeArray是内部使用的方法
	// 因为是内部方法,从代码分析可知没有校验results的合法性
	makeArray: function( arr, results ) {
		var ret = results || [];

		if ( arr != null ) {
			// Object(arr)会将arra转成类数组对象,转成功则调用merge合并ret跟arr
			if ( isArraylike( Object(arr) ) ) {
				jQuery.merge( ret,
					typeof arr === "string" ?
					[ arr ] : arr
				);
			// 否则直接push进ret
			} else {
				push.call( ret, arr );
			}
		}
		// 返回ret
		return ret;
	},

	// 判断elem是否在arr里,i表示从第arr[i]开始往后判断
	inArray: function( elem, arr, i ) {
		var len;

		if ( arr ) {
			// 存在indexOf则直接调用js Array 的indexof查找
			if ( indexOf ) {
				return indexOf.call( arr, elem, i );
			}
	
			// 不存在indexOf则重新实现一个
			len = arr.length;
			// 校验i的值,根据情况处理,正值直接使用,负值使用Math.max( 0, len + i )
			i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

			for ( ; i < len; i++ ) {
				// 判断是否查找成功
				if ( i in arr && arr[ i ] === elem ) {
					return i;
				}
			}
		}

		// 查找失败返回-1
		return -1;
	},

	// 合并数组或者类数组对象如{0:"a", 1:"b", length:2}(对象必须有length属性)
	merge: function( first, second ) {
		var len = +second.length,
			j = 0,
			i = first.length;

		while ( j < len ) {
			first[ i++ ] = second[ j++ ];
		}

		// 兼容在IE < 9下,存在类数组对象的length为NaN的,如NodeList(NaN !== NaN返回true,NaN == NaN是false)
		// 从这里可以看出如果first.length为NaN,则会出错,因此必须保证first是形式正确的类数组对象
		if ( len !== len ) {
			while ( second[j] !== undefined ) {
				first[ i++ ] = second[ j++ ];
			}
		}

		// 最终更新length并且返回first
		first.length = i;

		return first;
	},

	// elems中寻找满足callback的值,并返回数组集合,invert用于校验callback返回值,不传默认是true
	grep: function( elems, callback, invert ) {
		var callbackInverse,
			matches = [],
			i = 0,
			length = elems.length,
			// invert不传则为undefined,!invert则为true,即默认为true
			// 但如果传入值,则找相反的项
			callbackExpect = !invert;

		for ( ; i < length; i++ ) {
			// 遍历elems,将elems[ i ]和i传入callback,获取其返回值
			// 这里加上!是为了作数据转换,保证得到的是boolean值
			callbackInverse = !callback( elems[ i ], i );
			if ( callbackInverse !== callbackExpect ) {
				//满足条件塞进matches
				matches.push( elems[ i ] );
			}
		}

		return matches;
	},

	// elems里每一项均执行callback,返回其执行结果的集合
	// 参数arg仅供内部调用时使用
	map: function( elems, callback, arg ) {
		var value,
			i = 0,
			length = elems.length,
			isArray = isArraylike( elems ),
			ret = [];

		// 是类数组则遍历数组
		if ( isArray ) {
			for ( ; i < length; i++ ) {
				value = callback( elems[ i ], i, arg );

				if ( value != null ) {
					ret.push( value );
				}
			}

		// 非数组则用for in遍历
		} else {
			for ( i in elems ) {
				value = callback( elems[ i ], i, arg );

				if ( value != null ) {
					ret.push( value );
				}
			}
		}

		// 数组ret里的值有可能还是数组,使用concat可以进一步躺平
		// concat可通过传入多个参数连接多个数组,因此如果ret里的值全是数组,则传入ret相当于arguments
		return concat.apply( [], ret );
	},

	// A global GUID counter for objects
	guid: 1,

	// 绑定方法fn与其执行上下文context,返回一个可执行的方法proxy()
	proxy: function( fn, context ) {
		var args, proxy, tmp;

		// 如果传入context是字符串,则将其改为fn,而原fn改为其属性fn[ context ]
		if ( typeof context === "string" ) {
			tmp = fn[ context ];
			context = fn;
			fn = tmp;
		}

		// 校验入参fn,不正确返回undefined
		if ( !jQuery.isFunction( fn ) ) {
			return undefined;
		}

		// 截取arguments后面的值,作为fn调用时的参数
		// 注意这里的arguments是proxy: function( fn, context ) 的arguments
		args = slice.call( arguments, 2 );
		// 创建proxy,实现是执行fn.apply达到绑定context的效果
		proxy = function() {
			// 注意这里的arguments是proxy = function()的,与上面的不一样
			// 使用concat会返回新的数组,就不会影响proxy: function( fn, context )的参数
			return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
		};

		// 设置唯一的guid,方便日后删除对应的handler
		proxy.guid = fn.guid = fn.guid || jQuery.guid++;

		return proxy;
	},

	// 返回日期
	now: function() {
		return +( new Date() );
	},

	// 将之前定义的support对象传入jQuery命名空间中,作为工具使用
	support: support
});

// 建立class2type对象保存类型map:{"[object Object]": "object", "[object Number]": "number",...}
// 前面代码定义纯粹对象var class2type = {}; 以及var toString = class2type.toString;
// 调用toString()返回"[object Object]",而对toString使用call即toString.call(),如果传入Number类型则返回"[object Number]",如此类推,返回的全是BOM对象的字符串
// JQ利用这点来作类型判断,更详细知识原理暂时不懂
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
	class2type[ "[object " + name + "]" ] = name.toLowerCase();
});

// isArraylike是内部用于判断obj是否为类数组对象,string类型会报错(因为是内部使用,因此没必要严格校验,只需要根据内部使用的情况校验入参)
function isArraylike( obj ) {
	var length = obj.length,
		type = jQuery.type( obj );
	
	if ( type === "function" || jQuery.isWindow( obj ) ) {
		return false;
	}

	// nodeType === 1即是ELEMENT_NODE,但如果存在length,也返回true
	if ( obj.nodeType === 1 && length ) {
		return true;
	}

	// &&优先级高于||,因此可以分为三段,同时满足则返回true,我个人觉得length === 0是多余的,于之后length > 0冲突
	return type === "array" || length === 0 ||
		typeof length === "number" && length > 0 && ( length - 1 ) in obj;
}



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值