jQuery之extend方法深入分析--deep copy

557 阅读3分钟

jQuery版本 1.7.1

由于jQuery版本众多,这里只研究1.7.1版本的extend的实现方法

代码

extend方法介绍

extend方法在jQuery中有较多的应用,平时写代码时也会用到,主要是利用其提供的deep copy的功能

以下是jQuery中的代码详情

点击查看代码详情
jQuery.extend = function() {
	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;
		target = arguments[1] || {};
		// skip the boolean and the target
		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
	if ( length === i ) {
		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 = 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
	return target;
};

参数分析

  • target = arguments[0] || {} 表示赋值运算符的左操作数,也就是被赋值对象
  • i=1 表示赋值运算符的右操作数,也就是赋值对象,由于支持将多个对象赋值到同一个左操作数上,因此通过i表示当前正在赋值的索引
  • length=arguments.length函数参数的个数 由于JS中对于函数签名没有严格的限制,因此,不能通过提前定义函数参数类型来限制传入参数的类型,以及限制函数传入参数的个数,因此有了以下修正函数参数的方法:
  • typeof target === "boolean"也就是说jQuery.extend(true,...)这种方式进行调用时,传入true表示进行deep copy,进行target赋值左操作数和i赋值右操作数的修正

实现方式

  • 首先通过一个for循环来实现多个赋值右操作数的赋值操作
  • if((options = arguments[i])!=null)这里使用了变量类型自动转换的特性,过滤null和undefined对象
  • 使用强制相等符号进行判断,防止循环赋值,如果没有这个判断,有以下两种情况
    • 做了一个实验,在deep copy模式下能够正常拷贝
    var a = {a:1};
    var b = {b: a};
    var _b = $.extend(true, a, b);
    
    输出:
    {a:1, b:{a:1}}
    
    原因:分析代码可以看出,由于是deep copy的原因,jQuery.extend(...)的返回值已经与target不是同一个对象了,因此不会出现这种循环赋值的情况了
    • 在非deep copy模式下,会出现嵌套很深的情况
    var a = {a:1};
    var b = {b: a};
    var _b = $.extend(a, b);
    
    输出:
    {a:1, b:{a:1, b:{a:1, b:{a:1, b....}...}
    
  • 对于非null和undefined对象,遍历对象属性,将options[i]对象的属性值copy赋值到target对象对应的属性值上
    • 对于非deep copy, 过滤undefined属性值,然后进行赋值target[name]=options[name]
    • 对于deep copy
      • 会通过if(...&copy&..)过滤属性值可以转换为false的属性
        • 这里比较有意思的一个点,不知道是不是隐藏的bug,还是别有用意,就是当属性值为null时,不会进行deep copy操作,但是由于copy!==undefined是一个强类型的判断,null!==undefined为true,因此会进行属性值的赋值
      • 判断赋值右操作数的类型,以保证赋值后左操作数属性值的类型和右操作数属性值相同,这里对右操作数为对象和数组进行分别进行选择,当左操作数属性值和右操作数属性值类型不同时,会强制重新赋值一个空的对象或数组给左操作数
      • 通过递归来进行属性的递归赋值实现deep copy
  • 赋值结束后,返回左操作数

实战

属性值为null或者undefined时
var b = {a:undefined, b:null, c:1};
var _b = $.extend(true, {}, b);

输出:

{b: null, c: 1}

可见,虽然undefined属性值不会进行赋值,但是null属性值会进行赋值

左操作数属性值类型和右操作数不同
var b = {a:[1,2,3]};
var a = {a:{1:10,2:20,3:30,4:40}};
var _b = $.extend(true, a, b);

输出:

{a:[1,2,3]}
左操作数属性值类型和右操作数相同
var b = {a:[1,2,3]};
var a = {a:[10,20,30,40]};
var _b = $.extend(true, a, b);

输出:

{a:[1,2,3,40]}

总结

了解了extend方法的内部逻辑,用起来就更得心应手了;也可以发现即使是这么广泛使用的库中,代码逻辑也是有可以改进的地方,比如循环拷贝的判断,是否应该只针对非deep copy的情况